diff --git a/CHANGES.md b/CHANGES.md index 52bb657e1595..622382629513 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,8 @@ Change Log * Fixed a bug in imagery loading that could cause some or all of the globe to be missing when using an imagery layer that does not cover the entire globe. * Added support for rendering a water effect on Quantized-Mesh terrain tiles. * Added `pack` and `unpack` functions to `Matrix2` and `Matrix3`. +* Added camera-terrain collision detection/response when the camera reference frame is set. +* Added `ScreenSpaceCameraController.enableCollisionDetection` to enable/disable camera collision detection with terrain. ### 1.6 - 2015-02-02 diff --git a/Source/Scene/ScreenSpaceCameraController.js b/Source/Scene/ScreenSpaceCameraController.js index b1276183684c..ac2888528d2d 100644 --- a/Source/Scene/ScreenSpaceCameraController.js +++ b/Source/Scene/ScreenSpaceCameraController.js @@ -243,6 +243,12 @@ define([ * @default 7500000.0 */ this.minimumTrackBallHeight = 7500000.0; + /** + * Enables or disables camera collision detection with terrain. + * @type {Boolean} + * @default true + */ + this.enableCollisionDetection = true; this._scene = scene; this._globe = undefined; @@ -1559,18 +1565,30 @@ define([ var scratchAdjustHeightCartographic = new Cartographic(); function adjustHeightForTerrain(controller) { + if (!controller.enableCollisionDetection) { + return; + } + var scene = controller._scene; var mode = scene.mode; - var globe = controller._globe; + var globe = scene.globe; if (!defined(globe) || mode === SceneMode.SCENE2D || mode === SceneMode.MORPHING) { return; } var camera = scene.camera; - var ellipsoid = controller._ellipsoid; + var ellipsoid = globe.ellipsoid; var projection = scene.mapProjection; + var transform; + var mag; + if (!Matrix4.equals(camera.transform, Matrix4.IDENTITY)) { + transform = Matrix4.clone(camera.transform); + mag = Cartesian3.magnitude(camera.position); + camera._setTransform(Matrix4.IDENTITY); + } + var cartographic = scratchAdjustHeightCartographic; if (mode === SceneMode.SCENE3D) { ellipsoid.cartesianToCartographic(camera.position, cartographic); @@ -1578,25 +1596,29 @@ define([ projection.unproject(camera.position, cartographic); } - if (cartographic.height > controller.minimumCollisionTerrainHeight) { - return; - } - - var height = globe.getHeight(cartographic); - if (!defined(height)) { - return; - } - - height += controller.minimumZoomDistance; - if (cartographic.height >= height) { - return; + 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); + } + } + } } - cartographic.height = height; - if (mode === SceneMode.SCENE3D) { - ellipsoid.cartographicToCartesian(cartographic, camera.position); - } else { - projection.project(cartographic, camera.position); + if (defined(transform)) { + camera._setTransform(transform); + 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); } } diff --git a/Specs/Scene/ScreenSpaceCameraControllerSpec.js b/Specs/Scene/ScreenSpaceCameraControllerSpec.js index 40dddd8304d4..fb9a9ded8803 100644 --- a/Specs/Scene/ScreenSpaceCameraControllerSpec.js +++ b/Specs/Scene/ScreenSpaceCameraControllerSpec.js @@ -55,6 +55,13 @@ defineSuite([ this.mapProjection = new GeographicProjection(ellipsoid); }; + var MockGlobe = function(ellipsoid) { + this.ellipsoid = ellipsoid; + this.getHeight = function(cartographic) { + return 0.0; + }; + }; + beforeAll(function() { canvas = createCanvas(1024, 768); }); @@ -989,6 +996,94 @@ defineSuite([ expect(Cartesian3.magnitude(camera.position)).toBeGreaterThan(Cartesian3.magnitude(position)); }); + it('camera does not go below the terrain in 3D', function() { + setUp3D(); + scene.globe = new MockGlobe(scene.mapProjection.ellipsoid); + + updateController(); + + camera.setView({ + position : Cartesian3.fromDegrees(-72.0, 40.0, 1.0) + }); + + updateController(); + + expect(camera.positionCartographic.height).toEqualEpsilon(controller.minimumZoomDistance, CesiumMath.EPSILON7); + }); + + it('camera does not go below the terrain in CV', function() { + setUpCV(); + scene.globe = new MockGlobe(scene.mapProjection.ellipsoid); + + updateController(); + + camera.setView({ + position : Cartesian3.fromDegrees(-72.0, 40.0, 1.0) + }); + + updateController(); + + expect(camera.position.z).toEqualEpsilon(controller.minimumZoomDistance, CesiumMath.EPSILON7); + }); + + it('camera does go below the terrain in 3D when collision detection is disabled', function() { + setUp3D(); + scene.globe = new MockGlobe(scene.mapProjection.ellipsoid); + controller.enableCollisionDetection = false; + + updateController(); + + camera.setView({ + position : Cartesian3.fromDegrees(-72.0, 40.0, 1.0) + }); + + updateController(); + + expect(camera.positionCartographic.height).toBeLessThan(controller.minimumZoomDistance); + }); + + it('camera does go below the terrain in CV when collision detection is disabled', function() { + setUpCV(); + scene.globe = new MockGlobe(scene.mapProjection.ellipsoid); + controller.enableCollisionDetection = false; + + updateController(); + + camera.setView({ + position : Cartesian3.fromDegrees(-72.0, 40.0, 1.0) + }); + + updateController(); + + expect(camera.position.z).toBeLessThan(controller.minimumZoomDistance); + }); + + it('camera does not go below the terrain in 3D with the transform set', function() { + setUp3D(); + scene.globe = new MockGlobe(scene.mapProjection.ellipsoid); + + updateController(); + + camera.lookAt(Cartesian3.fromDegrees(-72.0, 40.0, 1.0), new Cartesian3(1.0, 1.0, -10.0)); + + updateController(); + + expect(camera.positionCartographic.height).toBeGreaterThanOrEqualTo(controller.minimumZoomDistance); + }); + + it('camera does not go below the terrain in CV with the transform set', function() { + setUpCV(); + scene.globe = new MockGlobe(scene.mapProjection.ellipsoid); + + updateController(); + + camera.lookAt(Cartesian3.fromDegrees(-72.0, 40.0, 1.0), new Cartesian3(1.0, 1.0, -10.0)); + + updateController(); + + expect(camera.positionWC.x).toBeGreaterThanOrEqualTo(controller.minimumZoomDistance); + }); + it('is destroyed', function() { expect(controller.isDestroyed()).toEqual(false); controller.destroy();