diff --git a/CHANGES.md b/CHANGES.md index 8b190f0d9e36..d6d23fc89831 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,7 @@ - Fixed sky atmosphere artifacts below the horizon. [#8866](https://github.com/CesiumGS/cesium/pull/8866) - Fixed ground primitives in orthographic mode. [#5110](https://github.com/CesiumGS/cesium/issues/5110) - Fixed the depth plane in orthographic mode. This improves the quality of polylines and other primitives that are rendered near the horizon. [8858](https://github.com/CesiumGS/cesium/pull/8858) +- Fixed camera controls when the camera is underground. [#8811](https://github.com/CesiumGS/cesium/pull/8811) ### 1.69.0 - 2020-05-01 diff --git a/Source/Scene/Camera.js b/Source/Scene/Camera.js index cbfedd8b73c8..668d41ec29dd 100644 --- a/Source/Scene/Camera.js +++ b/Source/Scene/Camera.js @@ -1145,6 +1145,7 @@ Camera.prototype._adjustOrthographicFrustum = function (zooming) { rayIntersection = globe.pickWorldCoordinates( ray, scene, + true, scratchRayIntersection ); diff --git a/Source/Scene/Globe.js b/Source/Scene/Globe.js index 9a42146beaca..117d6ed3cc67 100644 --- a/Source/Scene/Globe.js +++ b/Source/Scene/Globe.js @@ -612,12 +612,18 @@ var scratchSphereIntersectionResult = { * * @param {Ray} ray The ray to test for intersection. * @param {Scene} scene The scene. + * @param {Boolean} [cullBackFaces=true] Set to true to not pick back faces. * @param {Cartesian3} [result] The object onto which to store the result. * @returns {Cartesian3|undefined} The intersection or undefined if none was found. The returned position is in projected coordinates for 2D and Columbus View. * * @private */ -Globe.prototype.pickWorldCoordinates = function (ray, scene, result) { +Globe.prototype.pickWorldCoordinates = function ( + ray, + scene, + cullBackFaces, + result +) { //>>includeStart('debug', pragmas.debug); if (!defined(ray)) { throw new DeveloperError("ray is required"); @@ -627,6 +633,8 @@ Globe.prototype.pickWorldCoordinates = function (ray, scene, result) { } //>>includeEnd('debug'); + cullBackFaces = defaultValue(cullBackFaces, true); + var mode = scene.mode; var projection = scene.mapProjection; @@ -691,7 +699,7 @@ Globe.prototype.pickWorldCoordinates = function (ray, scene, result) { ray, scene.mode, scene.mapProjection, - true, + cullBackFaces, result ); if (defined(intersection)) { @@ -717,7 +725,7 @@ var cartoScratch = new Cartographic(); * var intersection = globe.pick(ray, scene); */ Globe.prototype.pick = function (ray, scene, result) { - result = this.pickWorldCoordinates(ray, scene, result); + result = this.pickWorldCoordinates(ray, scene, true, result); if (defined(result) && scene.mode !== SceneMode.SCENE3D) { result = Cartesian3.fromElements(result.y, result.z, result.x, result); var carto = scene.mapProjection.unproject(result, cartoScratch); diff --git a/Source/Scene/Scene.js b/Source/Scene/Scene.js index 61ece96a6826..ff85ad6a9c71 100644 --- a/Source/Scene/Scene.js +++ b/Source/Scene/Scene.js @@ -211,6 +211,9 @@ function Scene(options) { this._primitives = new PrimitiveCollection(); this._groundPrimitives = new PrimitiveCollection(); + this._globeHeight = undefined; + this._cameraUnderground = false; + this._logDepthBuffer = context.fragmentDepth; this._logDepthBufferDirty = true; @@ -1649,6 +1652,15 @@ Object.defineProperties(Scene.prototype, { return 0.9999; }, }, + + /** + * @private + */ + globeHeight: { + get: function () { + return this._globeHeight; + }, + }, }); /** @@ -3309,7 +3321,10 @@ Scene.prototype.updateEnvironment = function () { environmentState.isReadyForAtmosphere || globe._surface._tilesToRender.length > 0; } - environmentState.skyAtmosphereCommand = skyAtmosphere.update(frameState); + environmentState.skyAtmosphereCommand = skyAtmosphere.update( + frameState, + globe + ); if (defined(environmentState.skyAtmosphereCommand)) { this.updateDerivedCommands(environmentState.skyAtmosphereCommand); } @@ -3693,6 +3708,16 @@ function callAfterRenderFunctions(scene) { functions.length = 0; } +function getGlobeHeight(scene) { + var globe = scene._globe; + var camera = scene.camera; + var cartographic = camera.positionCartographic; + if (defined(globe) && globe.show && defined(cartographic)) { + return globe.getHeight(cartographic); + } + return undefined; +} + function isCameraUnderground(scene) { var camera = scene.camera; var mode = scene._mode; @@ -3700,6 +3725,10 @@ function isCameraUnderground(scene) { var cameraController = scene._screenSpaceCameraController; var cartographic = camera.positionCartographic; + if (!defined(cartographic)) { + return false; + } + if (!cameraController.onMap() && cartographic.height < 0.0) { // The camera can go off the map while in Columbus View. // Make a best guess as to whether it's underground by checking if its height is less than zero. @@ -3715,17 +3744,8 @@ function isCameraUnderground(scene) { return false; } - if (cameraController.adjustedHeightForTerrain()) { - // The camera controller already adjusted the camera, no need to call globe.getHeight again - return false; - } - - var globeHeight = globe.getHeight(cartographic); - if (defined(globeHeight) && cartographic.height < globeHeight) { - return true; - } - - return false; + var globeHeight = scene._globeHeight; + return defined(globeHeight) && cartographic.height < globeHeight; } /** @@ -3741,6 +3761,10 @@ Scene.prototype.initializeFrame = function () { this._tweens.update(); + this._globeHeight = getGlobeHeight(this); + this._cameraUnderground = isCameraUnderground(this); + this._globeTranslucencyState.update(this); + this._screenSpaceCameraController.update(); if (defined(this._deviceOrientationCameraController)) { this._deviceOrientationCameraController.update(); @@ -3748,10 +3772,6 @@ Scene.prototype.initializeFrame = function () { this.camera.update(this._mode); this.camera._updateCameraChanged(); - - this._cameraUnderground = isCameraUnderground(this); - - this._globeTranslucencyState.update(this); }; function updateDebugShowFramesPerSecond(scene, renderedThisFrame) { diff --git a/Source/Scene/SceneTransitioner.js b/Source/Scene/SceneTransitioner.js index fbb892b6a683..d2789fd815c7 100644 --- a/Source/Scene/SceneTransitioner.js +++ b/Source/Scene/SceneTransitioner.js @@ -644,6 +644,7 @@ function morphFromColumbusViewTo2D(transitioner, duration) { var pickPos = globe.pickWorldCoordinates( ray, scene, + true, scratchCVTo2DPickPos ); if (defined(pickPos)) { @@ -773,6 +774,7 @@ function morphFrom3DTo2D(transitioner, duration, ellipsoid) { var pickedPos = globe.pickWorldCoordinates( ray, scene, + true, scratch3DTo2DPickPosition ); if (defined(pickedPos)) { diff --git a/Source/Scene/ScreenSpaceCameraController.js b/Source/Scene/ScreenSpaceCameraController.js index 92e50e9eac8a..9866dde783a2 100644 --- a/Source/Scene/ScreenSpaceCameraController.js +++ b/Source/Scene/ScreenSpaceCameraController.js @@ -231,7 +231,7 @@ function ScreenSpaceCameraController(scene) { this._minimumCollisionTerrainHeight = this.minimumCollisionTerrainHeight; /** * The minimum height the camera must be before switching from rotating a track ball to - * free look when clicks originate on the sky on in space. + * free look when clicks originate on the sky or in space. * @type {Number} * @default 7500000.0 */ @@ -255,6 +255,20 @@ function ScreenSpaceCameraController(scene) { this._lastInertiaTranslateMovement = undefined; this._lastInertiaTiltMovement = undefined; + // Zoom disables tilt, spin, and translate inertia + // Tilt disables spin and translate inertia + this._inertiaDisablers = { + _lastInertiaZoomMovement: [ + "_lastInertiaSpinMovement", + "_lastInertiaTranslateMovement", + "_lastInertiaTiltMovement", + ], + _lastInertiaTiltMovement: [ + "_lastInertiaSpinMovement", + "_lastInertiaTranslateMovement", + ], + }; + this._tweens = new TweenCollection(); this._tween = undefined; @@ -265,6 +279,8 @@ function ScreenSpaceCameraController(scene) { this._rotateMousePosition = new Cartesian2(-1.0, -1.0); this._rotateStartPosition = new Cartesian3(); this._strafeStartPosition = new Cartesian3(); + this._strafeMousePosition = new Cartesian2(); + this._strafeEndMousePosition = new Cartesian2(); this._zoomMouseStart = new Cartesian2(-1.0, -1.0); this._zoomWorldPosition = new Cartesian3(); this._useZoomWorldPosition = false; @@ -273,8 +289,10 @@ function ScreenSpaceCameraController(scene) { this._rotating = false; this._strafing = false; this._zoomingOnVector = false; + this._zoomingUnderground = false; this._rotatingZoom = false; this._adjustedHeightForTerrain = false; + this._cameraUnderground = false; var projection = scene.mapProjection; this._maxCoord = projection.project( @@ -289,6 +307,8 @@ function ScreenSpaceCameraController(scene) { this._minimumRotateRate = 1.0 / 5000.0; this._minimumZoomRate = 20.0; this._maximumZoomRate = 5906376272000.0; // distance from the Sun to Pluto in meters. + this._minimumUndergroundPickDistance = 2000.0; + this._maximumUndergroundPickDistance = 10000.0; } function decay(time, coefficient) { @@ -329,7 +349,7 @@ function maintainInertia( startPosition: new Cartesian2(), endPosition: new Cartesian2(), motion: new Cartesian2(), - active: false, + inertiaEnabled: true, }; } @@ -343,56 +363,35 @@ function maintainInertia( if (ts && tr && threshold < inertiaMaxClickTimeThreshold) { var d = decay(fromNow, decayCoef); - if (!movementState.active) { - var lastMovement = aggregator.getLastMovement(type, modifier); - if (!defined(lastMovement) || sameMousePosition(lastMovement)) { - return; - } - - movementState.motion.x = - (lastMovement.endPosition.x - lastMovement.startPosition.x) * 0.5; - movementState.motion.y = - (lastMovement.endPosition.y - lastMovement.startPosition.y) * 0.5; - - movementState.startPosition = Cartesian2.clone( - lastMovement.startPosition, - movementState.startPosition - ); - - movementState.endPosition = Cartesian2.multiplyByScalar( - movementState.motion, - d, - movementState.endPosition - ); - movementState.endPosition = Cartesian2.add( - movementState.startPosition, - movementState.endPosition, - movementState.endPosition - ); + var lastMovement = aggregator.getLastMovement(type, modifier); + if ( + !defined(lastMovement) || + sameMousePosition(lastMovement) || + !movementState.inertiaEnabled + ) { + return; + } - movementState.active = true; - } else { - movementState.startPosition = Cartesian2.clone( - movementState.endPosition, - movementState.startPosition - ); + movementState.motion.x = + (lastMovement.endPosition.x - lastMovement.startPosition.x) * 0.5; + movementState.motion.y = + (lastMovement.endPosition.y - lastMovement.startPosition.y) * 0.5; - movementState.endPosition = Cartesian2.multiplyByScalar( - movementState.motion, - d, - movementState.endPosition - ); - movementState.endPosition = Cartesian2.add( - movementState.startPosition, - movementState.endPosition, - movementState.endPosition - ); + movementState.startPosition = Cartesian2.clone( + lastMovement.startPosition, + movementState.startPosition + ); - movementState.motion = Cartesian2.clone( - Cartesian2.ZERO, - movementState.motion - ); - } + movementState.endPosition = Cartesian2.multiplyByScalar( + movementState.motion, + d, + movementState.endPosition + ); + movementState.endPosition = Cartesian2.add( + movementState.startPosition, + movementState.endPosition, + movementState.endPosition + ); // If value from the decreasing exponential function is close to zero, // the end coordinates may be NaN. @@ -404,7 +403,6 @@ function maintainInertia( movementState.endPosition ) < 0.5 ) { - movementState.active = false; return; } @@ -412,8 +410,27 @@ function maintainInertia( var startPosition = aggregator.getStartMousePosition(type, modifier); action(object, startPosition, movementState); } - } else { - movementState.active = false; + } +} + +function activateInertia(controller, inertiaStateName) { + if (defined(inertiaStateName)) { + // Re-enable inertia if it was disabled + var movementState = controller[inertiaStateName]; + if (defined(movementState)) { + movementState.inertiaEnabled = true; + } + // Disable inertia on other movements + var inertiasToDisable = controller._inertiaDisablers[inertiaStateName]; + if (defined(inertiasToDisable)) { + var length = inertiasToDisable.length; + for (var i = 0; i < length; ++i) { + movementState = controller[inertiasToDisable[i]]; + if (defined(movementState)) { + movementState.inertiaEnabled = false; + } + } + } } } @@ -452,6 +469,7 @@ function reactToInput( if (controller.enableInputs && enabled) { if (movement) { action(controller, startPosition, movement); + activateInertia(controller, inertiaStateName); } else if (inertiaConstant < 1.0) { maintainInertia( aggregator, @@ -531,18 +549,24 @@ function handleZoom( rangeWindowRatio = Math.min(rangeWindowRatio, object.maximumMovementRatio); var distance = zoomRate * rangeWindowRatio; - if (distance > 0.0 && Math.abs(distanceMeasure - minHeight) < 1.0) { - return; - } + if ( + object.enableCollisionDetection || + object.minimumZoomDistance === 0.0 || + !defined(object._globe) // look-at mode + ) { + if (distance > 0.0 && Math.abs(distanceMeasure - minHeight) < 1.0) { + return; + } - if (distance < 0.0 && Math.abs(distanceMeasure - maxHeight) < 1.0) { - return; - } + if (distance < 0.0 && Math.abs(distanceMeasure - maxHeight) < 1.0) { + return; + } - if (distanceMeasure - distance < minHeight) { - distance = distanceMeasure - minHeight - 1.0; - } else if (distanceMeasure - distance > maxHeight) { - distance = distanceMeasure - maxHeight; + if (distanceMeasure - distance < minHeight) { + distance = distanceMeasure - minHeight - 1.0; + } else if (distanceMeasure - distance > maxHeight) { + distance = distanceMeasure - maxHeight; + } } var scene = object._scene; @@ -601,6 +625,7 @@ function handleZoom( zoomingOnVector = object._zoomingOnVector = false; rotatingZoom = object._rotatingZoom = false; + object._zoomingUnderground = object._cameraUnderground; } if (!object._useZoomWorldPosition) { @@ -660,8 +685,11 @@ function handleZoom( scratchCameraPositionNormal ); if ( - camera.positionCartographic.height < 3000.0 && - Math.abs(Cartesian3.dot(camera.direction, cameraPositionNormal)) < 0.6 + object._cameraUnderground || + object._zoomingUnderground || + (camera.positionCartographic.height < 3000.0 && + Math.abs(Cartesian3.dot(camera.direction, cameraPositionNormal)) < + 0.6) ) { zoomOnVector = true; } else { @@ -900,7 +928,9 @@ function handleZoom( camera.zoomIn(distance); } - camera.setView(scratchZoomViewOptions); + if (!object._cameraUnderground) { + camera.setView(scratchZoomViewOptions); + } } var translate2DStart = new Ray(); @@ -1070,6 +1100,8 @@ function pickGlobe(controller, mousePosition, result) { return undefined; } + var cullBackFaces = !controller._cameraUnderground; + var depthIntersection; if (scene.pickPositionSupported) { depthIntersection = scene.pickPositionWorldCoordinates( @@ -1082,6 +1114,7 @@ function pickGlobe(controller, mousePosition, result) { var rayIntersection = globe.pickWorldCoordinates( ray, scene, + cullBackFaces, scratchRayIntersection ); @@ -1099,11 +1132,108 @@ function pickGlobe(controller, mousePosition, result) { return Cartesian3.clone(rayIntersection, result); } +var scratchDistanceCartographic = new Cartographic(); + +function getDistanceFromSurface(controller) { + var ellipsoid = controller._ellipsoid; + var scene = controller._scene; + var camera = scene.camera; + var mode = scene.mode; + + var height = 0.0; + if (mode === SceneMode.SCENE3D) { + var cartographic = ellipsoid.cartesianToCartographic( + camera.position, + scratchDistanceCartographic + ); + if (defined(cartographic)) { + height = cartographic.height; + } + } else { + height = camera.position.z; + } + var globeHeight = defaultValue(controller._scene.globeHeight, 0.0); + var distanceFromSurface = Math.abs(globeHeight - height); + return distanceFromSurface; +} + +var scratchSurfaceNormal = new Cartesian3(); + +function getZoomDistanceUnderground(controller, ray) { + var origin = ray.origin; + var direction = ray.direction; + var distanceFromSurface = getDistanceFromSurface(controller); + + // Weight zoom distance based on how strongly the pick ray is pointing inward. + // Geocentric normal is accurate enough for these purposes + var surfaceNormal = Cartesian3.normalize(origin, scratchSurfaceNormal); + var strength = Math.abs(Cartesian3.dot(surfaceNormal, direction)); + strength = Math.max(strength, 0.5) * 2.0; + return distanceFromSurface * strength; +} + +function getTiltCenterUnderground(controller, ray, pickedPosition, result) { + var distance = Cartesian3.distance(ray.origin, pickedPosition); + var distanceFromSurface = getDistanceFromSurface(controller); + + var maximumDistance = CesiumMath.clamp( + distanceFromSurface * 5.0, + controller._minimumUndergroundPickDistance, + controller._maximumUndergroundPickDistance + ); + + if (distance > maximumDistance) { + // Simulate look-at behavior by tilting around a small invisible sphere + distance = Math.min(distance, distanceFromSurface / 5.0); + distance = Math.max(distance, 100.0); + } + + return Ray.getPoint(ray, distance, result); +} + +function getStrafeStartPositionUnderground( + controller, + ray, + pickedPosition, + result +) { + var distance; + if (!defined(pickedPosition)) { + distance = getDistanceFromSurface(controller); + } else { + distance = Cartesian3.distance(ray.origin, pickedPosition); + if (distance > controller._maximumUndergroundPickDistance) { + // If the picked position is too far away set the strafe speed based on the + // camera's height from the globe surface + distance = getDistanceFromSurface(controller); + } + } + + return Ray.getPoint(ray, distance, result); +} + +var scratchInertialDelta = new Cartesian2(); + +function continueStrafing(controller, movement) { + // Update the end position continually based on the inertial delta + var originalEndPosition = movement.endPosition; + var inertialDelta = Cartesian2.subtract( + movement.endPosition, + movement.startPosition, + scratchInertialDelta + ); + var endPosition = controller._strafeEndMousePosition; + Cartesian2.add(endPosition, inertialDelta, endPosition); + movement.endPosition = endPosition; + strafe(controller, movement, controller._strafeStartPosition); + movement.endPosition = originalEndPosition; +} + var translateCVStartRay = new Ray(); var translateCVEndRay = new Ray(); var translateCVStartPos = new Cartesian3(); var translateCVEndPos = new Cartesian3(); -var translatCVDifference = new Cartesian3(); +var translateCVDifference = new Cartesian3(); var translateCVOrigin = new Cartesian3(); var translateCVPlane = new Plane(Cartesian3.UNIT_X, 0.0); var translateCVStartMouse = new Cartesian2(); @@ -1124,12 +1254,13 @@ function translateCV(controller, startPosition, movement) { } if (controller._strafing) { - strafe(controller, startPosition, movement); + continueStrafing(controller, movement); return; } var scene = controller._scene; var camera = scene.camera; + var cameraUnderground = controller._cameraUnderground; var startMouse = Cartesian2.clone( movement.startPosition, translateCVStartMouse @@ -1148,14 +1279,24 @@ function translateCV(controller, startPosition, movement) { } } - if (origin.x > camera.position.z && defined(globePos)) { - Cartesian3.clone(globePos, controller._strafeStartPosition); + if ( + cameraUnderground || + (origin.x > camera.position.z && defined(globePos)) + ) { + var pickPosition = globePos; + if (cameraUnderground) { + pickPosition = getStrafeStartPositionUnderground( + controller, + startRay, + globePos, + translateCVStartPos + ); + } + Cartesian2.clone(startPosition, controller._strafeMousePosition); + Cartesian2.clone(startPosition, controller._strafeEndMousePosition); + Cartesian3.clone(pickPosition, controller._strafeStartPosition); controller._strafing = true; - strafe(controller, startPosition, movement); - controller._strafeMousePosition = Cartesian2.clone( - startPosition, - controller._strafeMousePosition - ); + strafe(controller, movement, controller._strafeStartPosition); return; } @@ -1185,7 +1326,7 @@ function translateCV(controller, startPosition, movement) { var diff = Cartesian3.subtract( startPlanePos, endPlanePos, - translatCVDifference + translateCVDifference ); var temp = diff.x; diff.x = diff.y; @@ -1234,7 +1375,7 @@ function rotateCV(controller, startPosition, movement) { if ( controller._tiltCVOffMap || !controller.onMap() || - camera.position.z > controller._minimumPickingTerrainHeight + Math.abs(camera.position.z) > controller._minimumPickingTerrainHeight ) { controller._tiltCVOffMap = true; rotateCVOnPlane(controller, startPosition, movement); @@ -1309,6 +1450,7 @@ function rotateCVOnPlane(controller, startPosition, movement) { function rotateCVOnTerrain(controller, startPosition, movement) { var scene = controller._scene; var camera = scene.camera; + var cameraUnderground = controller._cameraUnderground; var center; var ray; @@ -1343,6 +1485,13 @@ function rotateCVOnTerrain(controller, startPosition, movement) { Cartesian3.add(position, center, center); } + if (cameraUnderground) { + if (!defined(ray)) { + ray = camera.getPickRay(startPosition, rotateCVWindowRay); + } + getTiltCenterUnderground(controller, ray, center, center); + } + Cartesian2.clone(startPosition, controller._tiltCenterMousePosition); Cartesian3.clone(center, controller._tiltCenter); } @@ -1420,7 +1569,12 @@ function rotateCVOnTerrain(controller, startPosition, movement) { camera._setTransform(verticalTransform); if (dot < 0.0) { - if (movement.startPosition.y > movement.endPosition.y) { + var movementDelta = movement.startPosition.y - movement.endPosition.y; + if ( + (cameraUnderground && movementDelta < 0.0) || + (!cameraUnderground && movementDelta > 0.0) + ) { + // Prevent camera from flipping past the up axis constrainedAxis = undefined; } @@ -1518,23 +1672,48 @@ function zoomCV(controller, startPosition, movement) { var camera = scene.camera; var canvas = scene.canvas; - var windowPosition = zoomCVWindowPos; - windowPosition.x = canvas.clientWidth / 2; - windowPosition.y = canvas.clientHeight / 2; + var cameraUnderground = controller._cameraUnderground; + + var windowPosition; + + if (cameraUnderground) { + windowPosition = startPosition; + } else { + windowPosition = zoomCVWindowPos; + windowPosition.x = canvas.clientWidth / 2; + windowPosition.y = canvas.clientHeight / 2; + } + var ray = camera.getPickRay(windowPosition, zoomCVWindowRay); + var position = ray.origin; + var direction = ray.direction; + var height = camera.position.z; var intersection; - if (camera.position.z < controller._minimumPickingTerrainHeight) { + if (height < controller._minimumPickingTerrainHeight) { intersection = pickGlobe(controller, windowPosition, zoomCVIntersection); } var distance; if (defined(intersection)) { - distance = Cartesian3.distance(ray.origin, intersection); - } else { + distance = Cartesian3.distance(position, intersection); + } + + if (cameraUnderground) { + var distanceUnderground = getZoomDistanceUnderground( + controller, + ray, + height + ); + if (defined(distance)) { + distance = Math.min(distance, distanceUnderground); + } else { + distance = distanceUnderground; + } + } + + if (!defined(distance)) { var normal = Cartesian3.UNIT_X; - var position = ray.origin; - var direction = ray.direction; distance = -Cartesian3.dot(normal, position) / Cartesian3.dot(normal, direction); } @@ -1609,10 +1788,6 @@ function updateCV(controller) { if ( !controller._aggregator.anyButtonDown && - (!defined(controller._lastInertiaZoomMovement) || - !controller._lastInertiaZoomMovement.active) && - (!defined(controller._lastInertiaTranslateMovement) || - !controller._lastInertiaTranslateMovement.active) && !tweens.contains(controller._tween) ) { var tween = camera.createCorrectPositionTween( @@ -1633,21 +1808,11 @@ var scratchStrafeIntersection = new Cartesian3(); var scratchStrafeDirection = new Cartesian3(); var scratchMousePos = new Cartesian3(); -function strafe(controller, startPosition, movement) { +function strafe(controller, movement, strafeStartPosition) { var scene = controller._scene; var camera = scene.camera; - var mouseStartPosition = pickGlobe( - controller, - movement.startPosition, - scratchMousePos - ); - if (!defined(mouseStartPosition)) { - return; - } - - var mousePosition = movement.endPosition; - var ray = camera.getPickRay(mousePosition, scratchStrafeRay); + var ray = camera.getPickRay(movement.endPosition, scratchStrafeRay); var direction = Cartesian3.clone(camera.direction, scratchStrafeDirection); if (scene.mode === SceneMode.COLUMBUS_VIEW) { @@ -1655,7 +1820,7 @@ function strafe(controller, startPosition, movement) { } var plane = Plane.fromPointNormal( - mouseStartPosition, + strafeStartPosition, direction, scratchStrafePlane ); @@ -1668,7 +1833,7 @@ function strafe(controller, startPosition, movement) { return; } - direction = Cartesian3.subtract(mouseStartPosition, intersection, direction); + direction = Cartesian3.subtract(strafeStartPosition, intersection, direction); if (scene.mode === SceneMode.COLUMBUS_VIEW) { Cartesian3.fromElements(direction.y, direction.z, direction.x, direction); } @@ -1681,10 +1846,13 @@ var scratchCartographic = new Cartographic(); var scratchRadii = new Cartesian3(); var scratchEllipsoid = new Ellipsoid(); var scratchLookUp = new Cartesian3(); +var scratchNormal = new Cartesian3(); function spin3D(controller, startPosition, movement) { var scene = controller._scene; var camera = scene.camera; + var cameraUnderground = controller._cameraUnderground; + var ellipsoid = controller._ellipsoid; if (!Matrix4.equals(camera.transform, Matrix4.IDENTITY)) { rotate3D(controller, startPosition, movement); @@ -1693,34 +1861,8 @@ function spin3D(controller, startPosition, movement) { var magnitude; var radii; - var ellipsoid; - var up = controller._ellipsoid.geodeticSurfaceNormal( - camera.position, - scratchLookUp - ); - - var height = controller._ellipsoid.cartesianToCartographic( - camera.positionWC, - scratchCartographic - ).height; - var globe = controller._globe; - - var mousePos; - var tangentPick = false; - if (defined(globe) && height < controller._minimumPickingTerrainHeight) { - mousePos = pickGlobe(controller, movement.startPosition, scratchMousePos); - if (defined(mousePos)) { - var ray = camera.getPickRay(movement.startPosition, pickGlobeScratchRay); - var normal = controller._ellipsoid.geodeticSurfaceNormal(mousePos); - tangentPick = Math.abs(Cartesian3.dot(ray.direction, normal)) < 0.05; - - if (tangentPick && !controller._looking) { - controller._rotating = false; - controller._strafing = true; - } - } - } + var up = ellipsoid.geodeticSurfaceNormal(camera.position, scratchLookUp); if (Cartesian2.equals(startPosition, controller._rotateMousePosition)) { if (controller._looking) { @@ -1728,9 +1870,15 @@ function spin3D(controller, startPosition, movement) { } else if (controller._rotating) { rotate3D(controller, startPosition, movement); } else if (controller._strafing) { - Cartesian3.clone(mousePos, controller._strafeStartPosition); - strafe(controller, startPosition, movement); + continueStrafing(controller, movement); } else { + if ( + Cartesian3.magnitude(camera.position) < + Cartesian3.magnitude(controller._rotateStartPosition) + ) { + // Pan action is no longer valid if camera moves below the pan ellipsoid + return; + } magnitude = Cartesian3.magnitude(controller._rotateStartPosition); radii = scratchRadii; radii.x = radii.y = radii.z = magnitude; @@ -1743,15 +1891,44 @@ function spin3D(controller, startPosition, movement) { controller._rotating = false; controller._strafing = false; + var height = ellipsoid.cartesianToCartographic( + camera.positionWC, + scratchCartographic + ).height; + var globe = controller._globe; + if (defined(globe) && height < controller._minimumPickingTerrainHeight) { + var mousePos = pickGlobe( + controller, + movement.startPosition, + scratchMousePos + ); if (defined(mousePos)) { - if ( - Cartesian3.magnitude(camera.position) < Cartesian3.magnitude(mousePos) - ) { - Cartesian3.clone(mousePos, controller._strafeStartPosition); + var strafing = false; + var ray = camera.getPickRay(movement.startPosition, pickGlobeScratchRay); + + if (cameraUnderground) { + strafing = true; + getStrafeStartPositionUnderground(controller, ray, mousePos, mousePos); + } else { + var normal = ellipsoid.geodeticSurfaceNormal(mousePos, scratchNormal); + var tangentPick = + Math.abs(Cartesian3.dot(ray.direction, normal)) < 0.05; + + if (tangentPick) { + strafing = true; + } else { + strafing = + Cartesian3.magnitude(camera.position) < + Cartesian3.magnitude(mousePos); + } + } + if (strafing) { + Cartesian2.clone(startPosition, controller._strafeEndMousePosition); + Cartesian3.clone(mousePos, controller._strafeStartPosition); controller._strafing = true; - strafe(controller, startPosition, movement); + strafe(controller, movement, controller._strafeStartPosition); } else { magnitude = Cartesian3.magnitude(mousePos); radii = scratchRadii; @@ -1974,9 +2151,18 @@ function zoom3D(controller, startPosition, movement) { var camera = scene.camera; var canvas = scene.canvas; - var windowPosition = zoomCVWindowPos; - windowPosition.x = canvas.clientWidth / 2; - windowPosition.y = canvas.clientHeight / 2; + var cameraUnderground = controller._cameraUnderground; + + var windowPosition; + + if (cameraUnderground) { + windowPosition = startPosition; + } else { + windowPosition = zoomCVWindowPos; + windowPosition.x = canvas.clientWidth / 2; + windowPosition.y = canvas.clientHeight / 2; + } + var ray = camera.getPickRay(windowPosition, zoomCVWindowRay); var intersection; @@ -1991,7 +2177,22 @@ function zoom3D(controller, startPosition, movement) { var distance; if (defined(intersection)) { distance = Cartesian3.distance(ray.origin, intersection); - } else { + } + + if (cameraUnderground) { + var distanceUnderground = getZoomDistanceUnderground( + controller, + ray, + height + ); + if (defined(distance)) { + distance = Math.min(distance, distanceUnderground); + } else { + distance = distanceUnderground; + } + } + + if (!defined(distance)) { distance = height; } @@ -2149,6 +2350,7 @@ function tilt3DOnTerrain(controller, startPosition, movement) { var ellipsoid = controller._ellipsoid; var scene = controller._scene; var camera = scene.camera; + var cameraUnderground = controller._cameraUnderground; var center; var ray; @@ -2181,6 +2383,13 @@ function tilt3DOnTerrain(controller, startPosition, movement) { center = Ray.getPoint(ray, intersection.start, tilt3DCenter); } + if (cameraUnderground) { + if (!defined(ray)) { + ray = camera.getPickRay(startPosition, tilt3DRay); + } + getTiltCenterUnderground(controller, ray, center, center); + } + Cartesian2.clone(startPosition, controller._tiltCenterMousePosition); Cartesian3.clone(center, controller._tiltCenter); } @@ -2242,7 +2451,12 @@ function tilt3DOnTerrain(controller, startPosition, movement) { camera._setTransform(verticalTransform); if (dot < 0.0) { - if (movement.startPosition.y > movement.endPosition.y) { + var movementDelta = movement.startPosition.y - movement.endPosition.y; + if ( + (cameraUnderground && movementDelta < 0.0) || + (!cameraUnderground && movementDelta > 0.0) + ) { + // Prevent camera from flipping past the up axis constrainedAxis = undefined; } @@ -2527,9 +2741,9 @@ function adjustHeightForTerrain(controller) { var heightUpdated = false; if (cartographic.height < controller._minimumCollisionTerrainHeight) { - var height = globe.getHeight(cartographic); - if (defined(height)) { - height += controller.minimumZoomDistance; + var globeHeight = controller._scene.globeHeight; + if (defined(globeHeight)) { + var height = globeHeight + controller.minimumZoomDistance; if (cartographic.height < height) { cartographic.height = height; if (mode === SceneMode.SCENE3D) { @@ -2584,23 +2798,29 @@ var scratchPreviousDirection = new Cartesian3(); * @private */ ScreenSpaceCameraController.prototype.update = function () { - var camera = this._scene.camera; + var scene = this._scene; + var camera = scene.camera; + var globe = scene.globe; + var mode = scene.mode; + if (!Matrix4.equals(camera.transform, Matrix4.IDENTITY)) { this._globe = undefined; this._ellipsoid = Ellipsoid.UNIT_SPHERE; } else { - this._globe = this._scene.globe; + this._globe = globe; this._ellipsoid = defined(this._globe) ? this._globe.ellipsoid - : this._scene.mapProjection.ellipsoid; + : scene.mapProjection.ellipsoid; } + this._cameraUnderground = scene.cameraUnderground && defined(this._globe); + this._minimumCollisionTerrainHeight = - this.minimumCollisionTerrainHeight * this._scene.terrainExaggeration; + this.minimumCollisionTerrainHeight * scene.terrainExaggeration; this._minimumPickingTerrainHeight = - this.minimumPickingTerrainHeight * this._scene.terrainExaggeration; + this.minimumPickingTerrainHeight * scene.terrainExaggeration; this._minimumTrackBallHeight = - this.minimumTrackBallHeight * this._scene.terrainExaggeration; + this.minimumTrackBallHeight * scene.terrainExaggeration; var radius = this._ellipsoid.maximumRadius; this._rotateFactor = 1.0 / radius; @@ -2616,8 +2836,6 @@ ScreenSpaceCameraController.prototype.update = function () { scratchPreviousDirection ); - var scene = this._scene; - var mode = scene.mode; if (mode === SceneMode.SCENE2D) { update2D(this); } else if (mode === SceneMode.COLUMBUS_VIEW) { @@ -2629,7 +2847,7 @@ ScreenSpaceCameraController.prototype.update = function () { } 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 + // Adjust the camera height if the camera moved at all (user input or inertia) and an action didn't already adjust the camera height var cameraChanged = !Cartesian3.equals(previousPosition, camera.positionWC) || !Cartesian3.equals(previousDirection, camera.directionWC); @@ -2641,13 +2859,6 @@ ScreenSpaceCameraController.prototype.update = function () { this._aggregator.reset(); }; -/** - * @private - */ -ScreenSpaceCameraController.prototype.adjustedHeightForTerrain = function () { - return this._adjustedHeightForTerrain; -}; - /** * Returns true if this object was destroyed; otherwise, false. *

diff --git a/Source/Scene/SkyAtmosphere.js b/Source/Scene/SkyAtmosphere.js index 3cf73277e25c..33c9904c4cd6 100644 --- a/Source/Scene/SkyAtmosphere.js +++ b/Source/Scene/SkyAtmosphere.js @@ -166,7 +166,7 @@ var scratchModelMatrix = new Matrix4(); /** * @private */ -SkyAtmosphere.prototype.update = function (frameState) { +SkyAtmosphere.prototype.update = function (frameState, globe) { if (!this.show) { return undefined; } @@ -204,7 +204,8 @@ SkyAtmosphere.prototype.update = function (frameState) { var colorCorrect = hasColorCorrection(this); var translucent = frameState.globeTranslucencyState.translucent; - var perFragmentAtmosphere = this.perFragmentAtmosphere || translucent; + var perFragmentAtmosphere = + this.perFragmentAtmosphere || translucent || !defined(globe) || !globe.show; var command = this._command; diff --git a/Specs/Scene/SceneSpec.js b/Specs/Scene/SceneSpec.js index ab1a2a989115..65ba5488ea1d 100644 --- a/Specs/Scene/SceneSpec.js +++ b/Specs/Scene/SceneSpec.js @@ -2064,22 +2064,6 @@ describe( }); }); - it("detects that camera is above ground if screen space camera controller adjusted height for terrain", function () { - var scene = createScene(); - var globe = new Globe(); - scene.globe = globe; - - spyOn( - ScreenSpaceCameraController.prototype, - "adjustedHeightForTerrain" - ).and.returnValue(true); - - return updateGlobeUntilDone(scene).then(function () { - expect(scene.cameraUnderground).toBe(false); - scene.destroyForSpecs(); - }); - }); - it("detects that camera is above ground if globe is undefined", function () { var scene = createScene(); scene.renderForSpecs(); diff --git a/Specs/Scene/ScreenSpaceCameraControllerSpec.js b/Specs/Scene/ScreenSpaceCameraControllerSpec.js index cc0ea14890db..4881111f4d22 100644 --- a/Specs/Scene/ScreenSpaceCameraControllerSpec.js +++ b/Specs/Scene/ScreenSpaceCameraControllerSpec.js @@ -34,6 +34,8 @@ describe("Scene/ScreenSpaceCameraController", function () { this.mapProjection = new GeographicProjection(ellipsoid); this.terrainExaggeration = 1.0; this.screenSpaceCameraController = undefined; + this.cameraUnderground = false; + this.globeHeight = 0.0; } function MockGlobe(ellipsoid) { @@ -55,6 +57,7 @@ describe("Scene/ScreenSpaceCameraController", function () { tilesWaitingForChildren: 0, }, }; + this.show = true; } beforeAll(function () { usePointerEvents = FeatureDetection.supportsPointerEvents(); @@ -183,6 +186,8 @@ describe("Scene/ScreenSpaceCameraController", function () { mapProjection: scene.mapProjection, }; + scene.cameraUnderground = false; + var maxRadii = ellipsoid.maximumRadius; var frustum = new OrthographicOffCenterFrustum(); frustum.right = maxRadii * Math.PI; @@ -210,6 +215,9 @@ describe("Scene/ScreenSpaceCameraController", function () { mapProjection: scene.mapProjection, }; + scene.cameraUnderground = false; + controller.enableCollisionDetection = true; + var maxRadii = ellipsoid.maximumRadius; camera.position = new Cartesian3(0.0, 0.0, maxRadii); camera.direction = Cartesian3.negate(Cartesian3.UNIT_Z, new Cartesian3()); @@ -217,6 +225,31 @@ describe("Scene/ScreenSpaceCameraController", function () { camera.right = Cartesian3.clone(Cartesian3.UNIT_X); } + function setUpCVUnderground() { + scene.mode = SceneMode.COLUMBUS_VIEW; + + var ellipsoid = Ellipsoid.WGS84; + scene.globe = new MockGlobe(ellipsoid); + scene.mapProjection = new GeographicProjection(ellipsoid); + + scene.frameState = { + mode: scene.mode, + mapProjection: scene.mapProjection, + }; + + scene.cameraUnderground = true; + controller.enableCollisionDetection = false; + + camera.position = new Cartesian3(0.0, 0.0, -100.0); + camera.direction = Cartesian3.clone(Cartesian3.UNIT_Z); + camera.up = Cartesian3.clone(Cartesian3.UNIT_Y); + camera.right = Cartesian3.cross( + camera.direction, + camera.up, + new Cartesian3() + ); + } + function setUp3D() { scene.mode = SceneMode.SCENE3D; @@ -227,6 +260,21 @@ describe("Scene/ScreenSpaceCameraController", function () { mode: scene.mode, mapProjection: scene.mapProjection, }; + + scene.cameraUnderground = false; + controller.enableCollisionDetection = true; + } + + function setUp3DUnderground() { + setUp3D(); + scene.globe = new MockGlobe(scene.mapProjection.ellipsoid); + scene.cameraUnderground = true; + controller.enableCollisionDetection = false; + + camera.setView({ destination: Camera.DEFAULT_VIEW_RECTANGLE }); + var positionCart = Ellipsoid.WGS84.cartesianToCartographic(camera.position); + positionCart.height = -100.0; + camera.position = Ellipsoid.WGS84.cartographicToCartesian(positionCart); } it("constructor throws without a scene", function () { @@ -718,6 +766,26 @@ describe("Scene/ScreenSpaceCameraController", function () { expect(position.z).toEqual(camera.position.z); }); + it("translates in Columbus view when camera is underground", function () { + setUpCVUnderground(); + + var position = Cartesian3.clone(camera.position); + var startPosition = new Cartesian2( + canvas.clientWidth / 2, + canvas.clientHeight / 4 + ); + var endPosition = new Cartesian2( + canvas.clientWidth / 2, + canvas.clientHeight / 2 + ); + + moveMouse(MouseButtons.LEFT, startPosition, endPosition); + updateController(); + expect(position.x).toEqual(camera.position.x); + expect(position.y).not.toEqual(camera.position.y); + expect(position.z).toEqual(camera.position.z); + }); + it("looks in Columbus view", function () { setUpCV(); var position = Cartesian3.clone(camera.position); @@ -804,6 +872,24 @@ describe("Scene/ScreenSpaceCameraController", function () { expect(position.z).toBeLessThan(camera.position.z); }); + it("zoom in Columbus view when camera is underground", function () { + setUpCVUnderground(); + + var position = Cartesian3.clone(camera.position); + 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(position.z).toBeLessThan(camera.position.z); + }); + it("rotates in Columbus view", function () { setUpCV(); var startPosition = new Cartesian2( @@ -869,6 +955,25 @@ describe("Scene/ScreenSpaceCameraController", function () { ).toEqualEpsilon(camera.up, CesiumMath.EPSILON14); }); + it("rotates in Columbus view when camera is underground", function () { + setUpCVUnderground(); + camera.position.y = -100.0; + + var position = Cartesian3.clone(camera.position); + var startPosition = new Cartesian2( + canvas.clientWidth / 2, + canvas.clientHeight / 2 + ); + var endPosition = new Cartesian2( + (3 * canvas.clientWidth) / 8, + (3 * canvas.clientHeight) / 8 + ); + + moveMouse(MouseButtons.MIDDLE, startPosition, endPosition); + updateController(); + expect(camera.position).not.toEqual(position); + }); + it("zooms in Columus view with camera transform set", function () { setUpCV(); @@ -988,6 +1093,28 @@ describe("Scene/ScreenSpaceCameraController", function () { ).toEqualEpsilon(camera.up, CesiumMath.EPSILON14); }); + it("strafes in 3D when camera is underground", function () { + setUp3DUnderground(); + + var position = Cartesian3.clone(camera.position); + var direction = Cartesian3.clone(camera.direction); + + var startPosition = new Cartesian2( + canvas.clientWidth / 2, + canvas.clientHeight / 2 + ); + var endPosition = new Cartesian2( + (3 * canvas.clientWidth) / 8, + (3 * canvas.clientHeight) / 8 + ); + + moveMouse(MouseButtons.LEFT, startPosition, endPosition); + updateController(); + + expect(camera.position).not.toEqual(position); + expect(camera.direction).toEqual(direction); + }); + it("rotates in 3D", function () { setUp3D(); var position = Cartesian3.clone(camera.position); @@ -1225,6 +1352,34 @@ describe("Scene/ScreenSpaceCameraController", function () { expect(frustumWidth).toBeLessThan(camera.frustum.width); }); + it("zoom in 3D when camera is underground", function () { + setUp3DUnderground(); + + var position = Cartesian3.clone(camera.position); + var direction = Cartesian3.clone(camera.direction); + + 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(); + var vector = Cartesian3.subtract( + camera.position, + position, + new Cartesian3() + ); + var normalizedVector = Cartesian3.normalize(vector, vector); + + expect(normalizedVector).toEqualEpsilon(direction, CesiumMath.EPSILON2); + expect(camera.direction).toEqualEpsilon(direction, CesiumMath.EPSILON6); + }); + it("tilts in 3D", function () { setUp3D(); var position = Cartesian3.clone(camera.position); @@ -1319,6 +1474,27 @@ describe("Scene/ScreenSpaceCameraController", function () { ).toEqualEpsilon(camera.up, CesiumMath.EPSILON14); }); + it("tilts in 3D when camera is underground", function () { + setUp3DUnderground(); + + var position = Cartesian3.clone(camera.position); + var direction = Cartesian3.clone(camera.direction); + + var startPosition = new Cartesian2( + canvas.clientWidth / 2, + canvas.clientHeight / 2 + ); + var endPosition = new Cartesian2( + canvas.clientWidth / 2, + canvas.clientHeight / 4 + ); + + moveMouse(MouseButtons.MIDDLE, startPosition, endPosition); + updateController(); + expect(camera.position).not.toEqual(position); + expect(camera.direction).not.toEqual(direction); + }); + it("looks in 3D", function () { setUp3D(); var position = Cartesian3.clone(camera.position);