UNPKG

itowns

Version:

A JS/WebGL framework for 3D geospatial data visualization

252 lines (208 loc) 9.57 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard"); Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var THREE = _interopRequireWildcard(require("three")); var _Coordinates = _interopRequireDefault(require("../Core/Geographic/Coordinates")); var _DEMUtils = _interopRequireDefault(require("../Utils/DEMUtils")); /** * Wrapper around three.js camera to expose some geographic helpers. */ function updatePreSse(camera, height, fov) { // sse = projected geometric error on screen plane from distance // We're using an approximation, assuming that the geometric error of all // objects is perpendicular to the camera view vector (= we always compute // for worst case). // // screen plane object // | __ // | / \ // | geometric{| // < fov angle . } sse error {| | // | \__/ // | // |<---------------------> // | distance // // geometric_error * screen_width (resp. screen_height) // = --------------------------------------- // 2 * distance * tan (horizontal_fov / 2) (resp. vertical_fov) // // // We pre-compute the preSSE (= constant part of the screen space error formula) once here var verticalFOV = THREE.Math.degToRad(fov); var verticalPreSSE = height / (2.0 * Math.tan(verticalFOV * 0.5)); // Note: the preSSE for the horizontal FOV is the same value // focale = (this.height * 0.5) / Math.tan(verticalFOV * 0.5); // horizontalFOV = 2 * Math.atan(this.width * 0.5 / focale); // horizontalPreSSE = this.width / (2.0 * Math.tan(horizontalFOV * 0.5)); (1) // => replacing horizontalFOV in Math.tan(horizontalFOV * 0.5) // Math.tan(horizontalFOV * 0.5) = Math.tan(2 * Math.atan(this.width * 0.5 / focale) * 0.5) // = Math.tan(Math.atan(this.width * 0.5 / focale)) // = this.width * 0.5 / focale // => now replacing focale // = this.width * 0.5 / (this.height * 0.5) / Math.tan(verticalFOV * 0.5) // = Math.tan(verticalFOV * 0.5) * this.width / this.height // => back to (1) // horizontalPreSSE = this.width / (2.0 * Math.tan(verticalFOV * 0.5) * this.width / this.height) // = this.height / 2.0 * Math.tan(verticalFOV * 0.5) // = verticalPreSSE camera._preSSE = verticalPreSSE; } function Camera(crs, width, height) { var _this = this; var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; Object.defineProperty(this, 'crs', { get: function get() { return crs; } }); this.camera3D = options.camera ? options.camera : new THREE.PerspectiveCamera(30, width / height); this._viewMatrix = new THREE.Matrix4(); this.width = width; this.height = height; this._viewMatrixNeedsUpdate = true; this.resize(width, height); this._preSSE = Infinity; if (this.camera3D.isPerspectiveCamera) { var fov = this.camera3D.fov; Object.defineProperty(this.camera3D, 'fov', { get: function get() { return fov; }, set: function set(newFov) { fov = newFov; updatePreSse(_this, _this.height, fov); } }); } } Camera.prototype.resize = function (width, height) { this.width = width; this.height = height; var ratio = width / height; updatePreSse(this, this.height, this.camera3D.fov); if (this.camera3D.aspect !== ratio) { this.camera3D.aspect = ratio; if (this.camera3D.isOrthographicCamera) { var halfH = (this.camera3D.right - this.camera3D.left) * 0.5 / ratio; var y = (this.camera3D.top + this.camera3D.bottom) * 0.5; this.camera3D.top = y + halfH; this.camera3D.bottom = y - halfH; } } if (this.camera3D.updateProjectionMatrix) { this.camera3D.updateProjectionMatrix(); this._viewMatrixNeedsUpdate = true; } }; Camera.prototype.update = function () { // update matrix this.camera3D.updateMatrixWorld(); this._viewMatrixNeedsUpdate = true; }; /** * Return the position in the requested CRS, or in camera's CRS if undefined. * @param {string} crs if defined (e.g 'EPSG:4236') the camera position will be returned in this CRS * @return {Coordinates} Coordinates object holding camera's position */ Camera.prototype.position = function (crs) { return new _Coordinates["default"](this.crs, this.camera3D.position).as(crs || this.crs); }; /** * Set the position of the camera using a Coordinates object. * If you want to modify the position directly using x,y,z value then use camera.camera3D.position.set(x, y, z) * @param {Coordinates} position the new position of the camera */ Camera.prototype.setPosition = function (position) { this.camera3D.position.copy(position.as(this.crs)); }; var tmp = { frustum: new THREE.Frustum(), matrix: new THREE.Matrix4(), box3: new THREE.Box3() }; var points = [new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3()]; function projectBox3PointsInCameraSpace(camera, box3, matrixWorld) { // Projects points in camera space // We don't project directly on screen to avoid artifacts when projecting // points behind the near plane. var m = camera.camera3D.matrixWorldInverse; if (matrixWorld) { m = tmp.matrix.multiplyMatrices(camera.camera3D.matrixWorldInverse, matrixWorld); } points[0].set(box3.min.x, box3.min.y, box3.min.z).applyMatrix4(m); points[1].set(box3.min.x, box3.min.y, box3.max.z).applyMatrix4(m); points[2].set(box3.min.x, box3.max.y, box3.min.z).applyMatrix4(m); points[3].set(box3.min.x, box3.max.y, box3.max.z).applyMatrix4(m); points[4].set(box3.max.x, box3.min.y, box3.min.z).applyMatrix4(m); points[5].set(box3.max.x, box3.min.y, box3.max.z).applyMatrix4(m); points[6].set(box3.max.x, box3.max.y, box3.min.z).applyMatrix4(m); points[7].set(box3.max.x, box3.max.y, box3.max.z).applyMatrix4(m); // In camera space objects are along the -Z axis // So if min.z is > -near, the object is invisible var atLeastOneInFrontOfNearPlane = false; for (var i = 0; i < 8; i++) { if (points[i].z <= -camera.camera3D.near) { atLeastOneInFrontOfNearPlane = true; } else { // Clamp to near plane points[i].z = -camera.camera3D.near; } } return atLeastOneInFrontOfNearPlane ? points : undefined; } var ndcBox3 = new THREE.Box3(new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, 1, 1)); Camera.prototype.isBox3Visible = function (box3, matrixWorld) { return this.box3SizeOnScreen(box3, matrixWorld).intersectsBox(ndcBox3); }; Camera.prototype.isSphereVisible = function (sphere, matrixWorld) { if (this._viewMatrixNeedsUpdate) { // update visibility testing matrix this._viewMatrix.multiplyMatrices(this.camera3D.projectionMatrix, this.camera3D.matrixWorldInverse); this._viewMatrixNeedsUpdate = false; } if (matrixWorld) { tmp.matrix.multiplyMatrices(this._viewMatrix, matrixWorld); tmp.frustum.setFromMatrix(tmp.matrix); } else { tmp.frustum.setFromMatrix(this._viewMatrix); } return tmp.frustum.intersectsSphere(sphere); }; Camera.prototype.box3SizeOnScreen = function (box3, matrixWorld) { var pts = projectBox3PointsInCameraSpace(this, box3, matrixWorld); // All points are in front of the near plane -> box3 is invisible if (!pts) { return tmp.box3.makeEmpty(); } // Project points on screen for (var i = 0; i < 8; i++) { pts[i].applyMatrix4(this.camera3D.projectionMatrix); } return tmp.box3.setFromPoints(pts); }; /** * Test for collision between camera and a geometry layer (DTM/DSM) to adjust camera position * It could be modified later to handle an array of geometry layers * TODO Improve Coordinates class to handle altitude for any coordinate system (even projected one) * @param {view} view where we test the collision between geometry layers and the camera * @param {elevationLayer} elevationLayer (DTM/DSM) used to test the collision with the camera. Could be another geometry layer * @param {minDistanceCollision} minDistanceCollision the minimum distance allowed between the camera and the surface */ Camera.prototype.adjustAltitudeToAvoidCollisionWithLayer = function (view, elevationLayer, minDistanceCollision) { // We put the camera location in geographic by default to easily handle altitude. (Should be improved in Coordinates class for all ref) var camLocation = view.camera.position().as('EPSG:4326'); if (elevationLayer !== undefined) { var elevationUnderCamera = _DEMUtils["default"].getElevationValueAt(elevationLayer, camLocation); if (elevationUnderCamera != undefined) { var difElevation = camLocation.altitude - (elevationUnderCamera.z + minDistanceCollision); // We move the camera to avoid collisions if too close to terrain if (difElevation < 0) { camLocation.altitude = elevationUnderCamera.z + minDistanceCollision; view.camera.camera3D.position.copy(camLocation.as(view.referenceCrs)); view.notifyChange(this.camera3D); } } } }; var _default = Camera; exports["default"] = _default;