import BoundingSphere from "./BoundingSphere.js"; import Cartesian3 from "./Cartesian3.js"; import defaultValue from "./defaultValue.js"; import defined from "./defined.js"; import DeveloperError from "./DeveloperError.js"; import Ellipsoid from "./Ellipsoid.js"; import CesiumMath from "./Math.js"; import Rectangle from "./Rectangle.js"; import Visibility from "./Visibility.js"; /** * Creates an Occluder derived from an object's position and radius, as well as the camera position. * The occluder can be used to determine whether or not other objects are visible or hidden behind the * visible horizon defined by the occluder and camera position. * * @alias Occluder * * @param {BoundingSphere} occluderBoundingSphere The bounding sphere surrounding the occluder. * @param {Cartesian3} cameraPosition The coordinate of the viewer/camera. * * @constructor * * @example * // Construct an occluder one unit away from the origin with a radius of one. * const cameraPosition = Cesium.Cartesian3.ZERO; * const occluderBoundingSphere = new Cesium.BoundingSphere(new Cesium.Cartesian3(0, 0, -1), 1); * const occluder = new Cesium.Occluder(occluderBoundingSphere, cameraPosition); */ function Occluder(occluderBoundingSphere, cameraPosition) { //>>includeStart('debug', pragmas.debug); if (!defined(occluderBoundingSphere)) { throw new DeveloperError("occluderBoundingSphere is required."); } if (!defined(cameraPosition)) { throw new DeveloperError("camera position is required."); } //>>includeEnd('debug'); this._occluderPosition = Cartesian3.clone(occluderBoundingSphere.center); this._occluderRadius = occluderBoundingSphere.radius; this._horizonDistance = 0.0; this._horizonPlaneNormal = undefined; this._horizonPlanePosition = undefined; this._cameraPosition = undefined; // cameraPosition fills in the above values this.cameraPosition = cameraPosition; } const scratchCartesian3 = new Cartesian3(); Object.defineProperties(Occluder.prototype, { /** * The position of the occluder. * @memberof Occluder.prototype * @type {Cartesian3} */ position: { get: function () { return this._occluderPosition; }, }, /** * The radius of the occluder. * @memberof Occluder.prototype * @type {Number} */ radius: { get: function () { return this._occluderRadius; }, }, /** * The position of the camera. * @memberof Occluder.prototype * @type {Cartesian3} */ cameraPosition: { set: function (cameraPosition) { //>>includeStart('debug', pragmas.debug); if (!defined(cameraPosition)) { throw new DeveloperError("cameraPosition is required."); } //>>includeEnd('debug'); cameraPosition = Cartesian3.clone(cameraPosition, this._cameraPosition); const cameraToOccluderVec = Cartesian3.subtract( this._occluderPosition, cameraPosition, scratchCartesian3 ); let invCameraToOccluderDistance = Cartesian3.magnitudeSquared( cameraToOccluderVec ); const occluderRadiusSqrd = this._occluderRadius * this._occluderRadius; let horizonDistance; let horizonPlaneNormal; let horizonPlanePosition; if (invCameraToOccluderDistance > occluderRadiusSqrd) { horizonDistance = Math.sqrt( invCameraToOccluderDistance - occluderRadiusSqrd ); invCameraToOccluderDistance = 1.0 / Math.sqrt(invCameraToOccluderDistance); horizonPlaneNormal = Cartesian3.multiplyByScalar( cameraToOccluderVec, invCameraToOccluderDistance, scratchCartesian3 ); const nearPlaneDistance = horizonDistance * horizonDistance * invCameraToOccluderDistance; horizonPlanePosition = Cartesian3.add( cameraPosition, Cartesian3.multiplyByScalar( horizonPlaneNormal, nearPlaneDistance, scratchCartesian3 ), scratchCartesian3 ); } else { horizonDistance = Number.MAX_VALUE; } this._horizonDistance = horizonDistance; this._horizonPlaneNormal = horizonPlaneNormal; this._horizonPlanePosition = horizonPlanePosition; this._cameraPosition = cameraPosition; }, }, }); /** * Creates an occluder from a bounding sphere and the camera position. * * @param {BoundingSphere} occluderBoundingSphere The bounding sphere surrounding the occluder. * @param {Cartesian3} cameraPosition The coordinate of the viewer/camera. * @param {Occluder} [result] The object onto which to store the result. * @returns {Occluder} The occluder derived from an object's position and radius, as well as the camera position. */ Occluder.fromBoundingSphere = function ( occluderBoundingSphere, cameraPosition, result ) { //>>includeStart('debug', pragmas.debug); if (!defined(occluderBoundingSphere)) { throw new DeveloperError("occluderBoundingSphere is required."); } if (!defined(cameraPosition)) { throw new DeveloperError("camera position is required."); } //>>includeEnd('debug'); if (!defined(result)) { return new Occluder(occluderBoundingSphere, cameraPosition); } Cartesian3.clone(occluderBoundingSphere.center, result._occluderPosition); result._occluderRadius = occluderBoundingSphere.radius; result.cameraPosition = cameraPosition; return result; }; const tempVecScratch = new Cartesian3(); /** * Determines whether or not a point, the occludee, is hidden from view by the occluder. * * @param {Cartesian3} occludee The point surrounding the occludee object. * @returns {Boolean} true if the occludee is visible; otherwise false. * * * @example * const cameraPosition = new Cesium.Cartesian3(0, 0, 0); * const littleSphere = new Cesium.BoundingSphere(new Cesium.Cartesian3(0, 0, -1), 0.25); * const occluder = new Cesium.Occluder(littleSphere, cameraPosition); * const point = new Cesium.Cartesian3(0, 0, -3); * occluder.isPointVisible(point); //returns true * * @see Occluder#computeVisibility */ Occluder.prototype.isPointVisible = function (occludee) { if (this._horizonDistance !== Number.MAX_VALUE) { let tempVec = Cartesian3.subtract( occludee, this._occluderPosition, tempVecScratch ); let temp = this._occluderRadius; temp = Cartesian3.magnitudeSquared(tempVec) - temp * temp; if (temp > 0.0) { temp = Math.sqrt(temp) + this._horizonDistance; tempVec = Cartesian3.subtract(occludee, this._cameraPosition, tempVec); return temp * temp > Cartesian3.magnitudeSquared(tempVec); } } return false; }; const occludeePositionScratch = new Cartesian3(); /** * Determines whether or not a sphere, the occludee, is hidden from view by the occluder. * * @param {BoundingSphere} occludee The bounding sphere surrounding the occludee object. * @returns {Boolean} true if the occludee is visible; otherwise false. * * * @example * const cameraPosition = new Cesium.Cartesian3(0, 0, 0); * const littleSphere = new Cesium.BoundingSphere(new Cesium.Cartesian3(0, 0, -1), 0.25); * const occluder = new Cesium.Occluder(littleSphere, cameraPosition); * const bigSphere = new Cesium.BoundingSphere(new Cesium.Cartesian3(0, 0, -3), 1); * occluder.isBoundingSphereVisible(bigSphere); //returns true * * @see Occluder#computeVisibility */ Occluder.prototype.isBoundingSphereVisible = function (occludee) { const occludeePosition = Cartesian3.clone( occludee.center, occludeePositionScratch ); const occludeeRadius = occludee.radius; if (this._horizonDistance !== Number.MAX_VALUE) { let tempVec = Cartesian3.subtract( occludeePosition, this._occluderPosition, tempVecScratch ); let temp = this._occluderRadius - occludeeRadius; temp = Cartesian3.magnitudeSquared(tempVec) - temp * temp; if (occludeeRadius < this._occluderRadius) { if (temp > 0.0) { temp = Math.sqrt(temp) + this._horizonDistance; tempVec = Cartesian3.subtract( occludeePosition, this._cameraPosition, tempVec ); return ( temp * temp + occludeeRadius * occludeeRadius > Cartesian3.magnitudeSquared(tempVec) ); } return false; } // Prevent against the case where the occludee radius is larger than the occluder's; since this is // an uncommon case, the following code should rarely execute. if (temp > 0.0) { tempVec = Cartesian3.subtract( occludeePosition, this._cameraPosition, tempVec ); const tempVecMagnitudeSquared = Cartesian3.magnitudeSquared(tempVec); const occluderRadiusSquared = this._occluderRadius * this._occluderRadius; const occludeeRadiusSquared = occludeeRadius * occludeeRadius; if ( (this._horizonDistance * this._horizonDistance + occluderRadiusSquared) * occludeeRadiusSquared > tempVecMagnitudeSquared * occluderRadiusSquared ) { // The occludee is close enough that the occluder cannot possible occlude the occludee return true; } temp = Math.sqrt(temp) + this._horizonDistance; return temp * temp + occludeeRadiusSquared > tempVecMagnitudeSquared; } // The occludee completely encompasses the occluder return true; } return false; }; const tempScratch = new Cartesian3(); /** * Determine to what extent an occludee is visible (not visible, partially visible, or fully visible). * * @param {BoundingSphere} occludeeBS The bounding sphere of the occludee. * @returns {Visibility} Visibility.NONE if the occludee is not visible, * Visibility.PARTIAL if the occludee is partially visible, or * Visibility.FULL if the occludee is fully visible. * * * @example * const sphere1 = new Cesium.BoundingSphere(new Cesium.Cartesian3(0, 0, -1.5), 0.5); * const sphere2 = new Cesium.BoundingSphere(new Cesium.Cartesian3(0, 0, -2.5), 0.5); * const cameraPosition = new Cesium.Cartesian3(0, 0, 0); * const occluder = new Cesium.Occluder(sphere1, cameraPosition); * occluder.computeVisibility(sphere2); //returns Visibility.NONE * * @see Occluder#isVisible */ Occluder.prototype.computeVisibility = function (occludeeBS) { //>>includeStart('debug', pragmas.debug); if (!defined(occludeeBS)) { throw new DeveloperError("occludeeBS is required."); } //>>includeEnd('debug'); // If the occludee radius is larger than the occluders, this will return that // the entire ocludee is visible, even though that may not be the case, though this should // not occur too often. const occludeePosition = Cartesian3.clone(occludeeBS.center); const occludeeRadius = occludeeBS.radius; if (occludeeRadius > this._occluderRadius) { return Visibility.FULL; } if (this._horizonDistance !== Number.MAX_VALUE) { // The camera is outside the occluder let tempVec = Cartesian3.subtract( occludeePosition, this._occluderPosition, tempScratch ); let temp = this._occluderRadius - occludeeRadius; const occluderToOccludeeDistSqrd = Cartesian3.magnitudeSquared(tempVec); temp = occluderToOccludeeDistSqrd - temp * temp; if (temp > 0.0) { // The occludee is not completely inside the occluder // Check to see if the occluder completely hides the occludee temp = Math.sqrt(temp) + this._horizonDistance; tempVec = Cartesian3.subtract( occludeePosition, this._cameraPosition, tempVec ); const cameraToOccludeeDistSqrd = Cartesian3.magnitudeSquared(tempVec); if ( temp * temp + occludeeRadius * occludeeRadius < cameraToOccludeeDistSqrd ) { return Visibility.NONE; } // Check to see whether the occluder is fully or partially visible // when the occludee does not intersect the occluder temp = this._occluderRadius + occludeeRadius; temp = occluderToOccludeeDistSqrd - temp * temp; if (temp > 0.0) { // The occludee does not intersect the occluder. temp = Math.sqrt(temp) + this._horizonDistance; return cameraToOccludeeDistSqrd < temp * temp + occludeeRadius * occludeeRadius ? Visibility.FULL : Visibility.PARTIAL; } //Check to see if the occluder is fully or partially visible when the occludee DOES //intersect the occluder tempVec = Cartesian3.subtract( occludeePosition, this._horizonPlanePosition, tempVec ); return Cartesian3.dot(tempVec, this._horizonPlaneNormal) > -occludeeRadius ? Visibility.PARTIAL : Visibility.FULL; } } return Visibility.NONE; }; const occludeePointScratch = new Cartesian3(); /** * Computes a point that can be used as the occludee position to the visibility functions. * Use a radius of zero for the occludee radius. Typically, a user computes a bounding sphere around * an object that is used for visibility; however it is also possible to compute a point that if * seen/not seen would also indicate if an object is visible/not visible. This function is better * called for objects that do not move relative to the occluder and is large, such as a chunk of * terrain. You are better off not calling this and using the object's bounding sphere for objects * such as a satellite or ground vehicle. * * @param {BoundingSphere} occluderBoundingSphere The bounding sphere surrounding the occluder. * @param {Cartesian3} occludeePosition The point where the occludee (bounding sphere of radius 0) is located. * @param {Cartesian3[]} positions List of altitude points on the horizon near the surface of the occluder. * @returns {Object} An object containing two attributes: occludeePoint and valid * which is a boolean value. * * @exception {DeveloperError} positions must contain at least one element. * @exception {DeveloperError} occludeePosition must have a value other than occluderBoundingSphere.center. * * @example * const cameraPosition = new Cesium.Cartesian3(0, 0, 0); * const occluderBoundingSphere = new Cesium.BoundingSphere(new Cesium.Cartesian3(0, 0, -8), 2); * const occluder = new Cesium.Occluder(occluderBoundingSphere, cameraPosition); * const positions = [new Cesium.Cartesian3(-0.25, 0, -5.3), new Cesium.Cartesian3(0.25, 0, -5.3)]; * const tileOccluderSphere = Cesium.BoundingSphere.fromPoints(positions); * const occludeePosition = tileOccluderSphere.center; * const occludeePt = Cesium.Occluder.computeOccludeePoint(occluderBoundingSphere, occludeePosition, positions); */ Occluder.computeOccludeePoint = function ( occluderBoundingSphere, occludeePosition, positions ) { //>>includeStart('debug', pragmas.debug); if (!defined(occluderBoundingSphere)) { throw new DeveloperError("occluderBoundingSphere is required."); } if (!defined(positions)) { throw new DeveloperError("positions is required."); } if (positions.length === 0) { throw new DeveloperError("positions must contain at least one element"); } //>>includeEnd('debug'); const occludeePos = Cartesian3.clone(occludeePosition); const occluderPosition = Cartesian3.clone(occluderBoundingSphere.center); const occluderRadius = occluderBoundingSphere.radius; const numPositions = positions.length; //>>includeStart('debug', pragmas.debug); if (Cartesian3.equals(occluderPosition, occludeePosition)) { throw new DeveloperError( "occludeePosition must be different than occluderBoundingSphere.center" ); } //>>includeEnd('debug'); // Compute a plane with a normal from the occluder to the occludee position. const occluderPlaneNormal = Cartesian3.normalize( Cartesian3.subtract(occludeePos, occluderPosition, occludeePointScratch), occludeePointScratch ); const occluderPlaneD = -Cartesian3.dot(occluderPlaneNormal, occluderPosition); //For each position, determine the horizon intersection. Choose the position and intersection //that results in the greatest angle with the occcluder plane. const aRotationVector = Occluder._anyRotationVector( occluderPosition, occluderPlaneNormal, occluderPlaneD ); let dot = Occluder._horizonToPlaneNormalDotProduct( occluderBoundingSphere, occluderPlaneNormal, occluderPlaneD, aRotationVector, positions[0] ); if (!dot) { //The position is inside the mimimum radius, which is invalid return undefined; } let tempDot; for (let i = 1; i < numPositions; ++i) { tempDot = Occluder._horizonToPlaneNormalDotProduct( occluderBoundingSphere, occluderPlaneNormal, occluderPlaneD, aRotationVector, positions[i] ); if (!tempDot) { //The position is inside the minimum radius, which is invalid return undefined; } if (tempDot < dot) { dot = tempDot; } } //Verify that the dot is not near 90 degress if (dot < 0.00174532836589830883577820272085) { return undefined; } const distance = occluderRadius / dot; return Cartesian3.add( occluderPosition, Cartesian3.multiplyByScalar( occluderPlaneNormal, distance, occludeePointScratch ), occludeePointScratch ); }; const computeOccludeePointFromRectangleScratch = []; /** * Computes a point that can be used as the occludee position to the visibility functions from a rectangle. * * @param {Rectangle} rectangle The rectangle used to create a bounding sphere. * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid used to determine positions of the rectangle. * @returns {Object} An object containing two attributes: occludeePoint and valid * which is a boolean value. */ Occluder.computeOccludeePointFromRectangle = function (rectangle, ellipsoid) { //>>includeStart('debug', pragmas.debug); if (!defined(rectangle)) { throw new DeveloperError("rectangle is required."); } //>>includeEnd('debug'); ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84); const positions = Rectangle.subsample( rectangle, ellipsoid, 0.0, computeOccludeePointFromRectangleScratch ); const bs = BoundingSphere.fromPoints(positions); // TODO: get correct ellipsoid center const ellipsoidCenter = Cartesian3.ZERO; if (!Cartesian3.equals(ellipsoidCenter, bs.center)) { return Occluder.computeOccludeePoint( new BoundingSphere(ellipsoidCenter, ellipsoid.minimumRadius), bs.center, positions ); } return undefined; }; const tempVec0Scratch = new Cartesian3(); Occluder._anyRotationVector = function ( occluderPosition, occluderPlaneNormal, occluderPlaneD ) { const tempVec0 = Cartesian3.abs(occluderPlaneNormal, tempVec0Scratch); let majorAxis = tempVec0.x > tempVec0.y ? 0 : 1; if ( (majorAxis === 0 && tempVec0.z > tempVec0.x) || (majorAxis === 1 && tempVec0.z > tempVec0.y) ) { majorAxis = 2; } const tempVec = new Cartesian3(); let tempVec1; if (majorAxis === 0) { tempVec0.x = occluderPosition.x; tempVec0.y = occluderPosition.y + 1.0; tempVec0.z = occluderPosition.z + 1.0; tempVec1 = Cartesian3.UNIT_X; } else if (majorAxis === 1) { tempVec0.x = occluderPosition.x + 1.0; tempVec0.y = occluderPosition.y; tempVec0.z = occluderPosition.z + 1.0; tempVec1 = Cartesian3.UNIT_Y; } else { tempVec0.x = occluderPosition.x + 1.0; tempVec0.y = occluderPosition.y + 1.0; tempVec0.z = occluderPosition.z; tempVec1 = Cartesian3.UNIT_Z; } const u = (Cartesian3.dot(occluderPlaneNormal, tempVec0) + occluderPlaneD) / -Cartesian3.dot(occluderPlaneNormal, tempVec1); return Cartesian3.normalize( Cartesian3.subtract( Cartesian3.add( tempVec0, Cartesian3.multiplyByScalar(tempVec1, u, tempVec), tempVec0 ), occluderPosition, tempVec0 ), tempVec0 ); }; const posDirectionScratch = new Cartesian3(); Occluder._rotationVector = function ( occluderPosition, occluderPlaneNormal, occluderPlaneD, position, anyRotationVector ) { //Determine the angle between the occluder plane normal and the position direction let positionDirection = Cartesian3.subtract( position, occluderPosition, posDirectionScratch ); positionDirection = Cartesian3.normalize( positionDirection, positionDirection ); if ( Cartesian3.dot(occluderPlaneNormal, positionDirection) < 0.99999998476912904932780850903444 ) { const crossProduct = Cartesian3.cross( occluderPlaneNormal, positionDirection, positionDirection ); const length = Cartesian3.magnitude(crossProduct); if (length > CesiumMath.EPSILON13) { return Cartesian3.normalize(crossProduct, new Cartesian3()); } } //The occluder plane normal and the position direction are colinear. Use any //vector in the occluder plane as the rotation vector return anyRotationVector; }; const posScratch1 = new Cartesian3(); const occluerPosScratch = new Cartesian3(); const posScratch2 = new Cartesian3(); const horizonPlanePosScratch = new Cartesian3(); Occluder._horizonToPlaneNormalDotProduct = function ( occluderBS, occluderPlaneNormal, occluderPlaneD, anyRotationVector, position ) { const pos = Cartesian3.clone(position, posScratch1); const occluderPosition = Cartesian3.clone( occluderBS.center, occluerPosScratch ); const occluderRadius = occluderBS.radius; //Verify that the position is outside the occluder let positionToOccluder = Cartesian3.subtract( occluderPosition, pos, posScratch2 ); const occluderToPositionDistanceSquared = Cartesian3.magnitudeSquared( positionToOccluder ); const occluderRadiusSquared = occluderRadius * occluderRadius; if (occluderToPositionDistanceSquared < occluderRadiusSquared) { return false; } //Horizon parameters const horizonDistanceSquared = occluderToPositionDistanceSquared - occluderRadiusSquared; const horizonDistance = Math.sqrt(horizonDistanceSquared); const occluderToPositionDistance = Math.sqrt( occluderToPositionDistanceSquared ); const invOccluderToPositionDistance = 1.0 / occluderToPositionDistance; const cosTheta = horizonDistance * invOccluderToPositionDistance; const horizonPlaneDistance = cosTheta * horizonDistance; positionToOccluder = Cartesian3.normalize( positionToOccluder, positionToOccluder ); const horizonPlanePosition = Cartesian3.add( pos, Cartesian3.multiplyByScalar( positionToOccluder, horizonPlaneDistance, horizonPlanePosScratch ), horizonPlanePosScratch ); const horizonCrossDistance = Math.sqrt( horizonDistanceSquared - horizonPlaneDistance * horizonPlaneDistance ); //Rotate the position to occluder vector 90 degrees let tempVec = this._rotationVector( occluderPosition, occluderPlaneNormal, occluderPlaneD, pos, anyRotationVector ); let horizonCrossDirection = Cartesian3.fromElements( tempVec.x * tempVec.x * positionToOccluder.x + (tempVec.x * tempVec.y - tempVec.z) * positionToOccluder.y + (tempVec.x * tempVec.z + tempVec.y) * positionToOccluder.z, (tempVec.x * tempVec.y + tempVec.z) * positionToOccluder.x + tempVec.y * tempVec.y * positionToOccluder.y + (tempVec.y * tempVec.z - tempVec.x) * positionToOccluder.z, (tempVec.x * tempVec.z - tempVec.y) * positionToOccluder.x + (tempVec.y * tempVec.z + tempVec.x) * positionToOccluder.y + tempVec.z * tempVec.z * positionToOccluder.z, posScratch1 ); horizonCrossDirection = Cartesian3.normalize( horizonCrossDirection, horizonCrossDirection ); //Horizon positions const offset = Cartesian3.multiplyByScalar( horizonCrossDirection, horizonCrossDistance, posScratch1 ); tempVec = Cartesian3.normalize( Cartesian3.subtract( Cartesian3.add(horizonPlanePosition, offset, posScratch2), occluderPosition, posScratch2 ), posScratch2 ); const dot0 = Cartesian3.dot(occluderPlaneNormal, tempVec); tempVec = Cartesian3.normalize( Cartesian3.subtract( Cartesian3.subtract(horizonPlanePosition, offset, tempVec), occluderPosition, tempVec ), tempVec ); const dot1 = Cartesian3.dot(occluderPlaneNormal, tempVec); return dot0 < dot1 ? dot0 : dot1; }; export default Occluder;