babylon.framingBehavior.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. module BABYLON {
  2. /**
  3. * The framing behavior (BABYLON.FramingBehavior) is designed to automatically position an ArcRotateCamera when its target is set to a mesh. It is also useful if you want to prevent the camera to go under a virtual horizontal plane.
  4. * @see http://doc.babylonjs.com/how_to/camera_behaviors#framing-behavior
  5. */
  6. export class FramingBehavior implements Behavior<ArcRotateCamera> {
  7. /**
  8. * Gets the name of the behavior.
  9. */
  10. public get name(): string {
  11. return "Framing";
  12. }
  13. private _mode = FramingBehavior.FitFrustumSidesMode;
  14. private _radiusScale = 1.0;
  15. private _positionScale = 0.5;
  16. private _defaultElevation = 0.3;
  17. private _elevationReturnTime = 1500;
  18. private _elevationReturnWaitTime = 1000;
  19. private _zoomStopsAnimation = false;
  20. private _framingTime = 1500;
  21. /**
  22. * The easing function used by animations
  23. */
  24. public static EasingFunction = new ExponentialEase();
  25. /**
  26. * The easing mode used by animations
  27. */
  28. public static EasingMode = EasingFunction.EASINGMODE_EASEINOUT;
  29. /**
  30. * Sets the current mode used by the behavior
  31. */
  32. public set mode(mode: number) {
  33. this._mode = mode;
  34. }
  35. /**
  36. * Gets current mode used by the behavior.
  37. */
  38. public get mode(): number {
  39. return this._mode;
  40. }
  41. /**
  42. * Sets the scale applied to the radius (1 by default)
  43. */
  44. public set radiusScale(radius: number) {
  45. this._radiusScale = radius;
  46. }
  47. /**
  48. * Gets the scale applied to the radius
  49. */
  50. public get radiusScale(): number {
  51. return this._radiusScale;
  52. }
  53. /**
  54. * Sets the scale to apply on Y axis to position camera focus. 0.5 by default which means the center of the bounding box.
  55. */
  56. public set positionScale(scale: number) {
  57. this._positionScale = scale;
  58. }
  59. /**
  60. * Gets the scale to apply on Y axis to position camera focus. 0.5 by default which means the center of the bounding box.
  61. */
  62. public get positionScale(): number {
  63. return this._positionScale;
  64. }
  65. /**
  66. * Sets the angle above/below the horizontal plane to return to when the return to default elevation idle
  67. * behaviour is triggered, in radians.
  68. */
  69. public set defaultElevation(elevation: number) {
  70. this._defaultElevation = elevation;
  71. }
  72. /**
  73. * Gets the angle above/below the horizontal plane to return to when the return to default elevation idle
  74. * behaviour is triggered, in radians.
  75. */
  76. public get defaultElevation() {
  77. return this._defaultElevation;
  78. }
  79. /**
  80. * Sets the time (in milliseconds) taken to return to the default beta position.
  81. * Negative value indicates camera should not return to default.
  82. */
  83. public set elevationReturnTime(speed: number) {
  84. this._elevationReturnTime = speed;
  85. }
  86. /**
  87. * Gets the time (in milliseconds) taken to return to the default beta position.
  88. * Negative value indicates camera should not return to default.
  89. */
  90. public get elevationReturnTime(): number {
  91. return this._elevationReturnTime;
  92. }
  93. /**
  94. * Sets the delay (in milliseconds) taken before the camera returns to the default beta position.
  95. */
  96. public set elevationReturnWaitTime(time: number) {
  97. this._elevationReturnWaitTime = time;
  98. }
  99. /**
  100. * Gets the delay (in milliseconds) taken before the camera returns to the default beta position.
  101. */
  102. public get elevationReturnWaitTime(): number {
  103. return this._elevationReturnWaitTime;
  104. }
  105. /**
  106. * Sets the flag that indicates if user zooming should stop animation.
  107. */
  108. public set zoomStopsAnimation(flag: boolean) {
  109. this._zoomStopsAnimation = flag;
  110. }
  111. /**
  112. * Gets the flag that indicates if user zooming should stop animation.
  113. */
  114. public get zoomStopsAnimation(): boolean {
  115. return this._zoomStopsAnimation;
  116. }
  117. /**
  118. * Sets the transition time when framing the mesh, in milliseconds
  119. */
  120. public set framingTime(time: number) {
  121. this._framingTime = time;
  122. }
  123. /**
  124. * Gets the transition time when framing the mesh, in milliseconds
  125. */
  126. public get framingTime() {
  127. return this._framingTime;
  128. }
  129. /**
  130. * Define if the behavior should automatically change the configured
  131. * camera limits and sensibilities.
  132. */
  133. public autoCorrectCameraLimitsAndSensibility = true;
  134. // Default behavior functions
  135. private _onPrePointerObservableObserver: Nullable<Observer<PointerInfoPre>>;
  136. private _onAfterCheckInputsObserver: Nullable<Observer<Camera>>;
  137. private _onMeshTargetChangedObserver: Nullable<Observer<Nullable<AbstractMesh>>>;
  138. private _attachedCamera: Nullable<ArcRotateCamera>;
  139. private _isPointerDown = false;
  140. private _lastInteractionTime = -Infinity;
  141. /**
  142. * Initializes the behavior.
  143. */
  144. public init(): void {
  145. // Do notihng
  146. }
  147. /**
  148. * Attaches the behavior to its arc rotate camera.
  149. * @param camera Defines the camera to attach the behavior to
  150. */
  151. public attach(camera: ArcRotateCamera): void {
  152. this._attachedCamera = camera;
  153. let scene = this._attachedCamera.getScene();
  154. FramingBehavior.EasingFunction.setEasingMode(FramingBehavior.EasingMode);
  155. this._onPrePointerObservableObserver = scene.onPrePointerObservable.add((pointerInfoPre) => {
  156. if (pointerInfoPre.type === PointerEventTypes.POINTERDOWN) {
  157. this._isPointerDown = true;
  158. return
  159. }
  160. if (pointerInfoPre.type === PointerEventTypes.POINTERUP) {
  161. this._isPointerDown = false;
  162. }
  163. });
  164. this._onMeshTargetChangedObserver = camera.onMeshTargetChangedObservable.add((mesh) => {
  165. if (mesh) {
  166. this.zoomOnMesh(mesh);
  167. }
  168. });
  169. this._onAfterCheckInputsObserver = camera.onAfterCheckInputsObservable.add(() => {
  170. // Stop the animation if there is user interaction and the animation should stop for this interaction
  171. this._applyUserInteraction();
  172. // Maintain the camera above the ground. If the user pulls the camera beneath the ground plane, lift it
  173. // back to the default position after a given timeout
  174. this._maintainCameraAboveGround();
  175. });
  176. }
  177. /**
  178. * Detaches the behavior from its current arc rotate camera.
  179. */
  180. public detach(): void {
  181. if (!this._attachedCamera) {
  182. return;
  183. }
  184. let scene = this._attachedCamera.getScene();
  185. if (this._onPrePointerObservableObserver) {
  186. scene.onPrePointerObservable.remove(this._onPrePointerObservableObserver);
  187. }
  188. if (this._onAfterCheckInputsObserver) {
  189. this._attachedCamera.onAfterCheckInputsObservable.remove(this._onAfterCheckInputsObserver);
  190. }
  191. if (this._onMeshTargetChangedObserver) {
  192. this._attachedCamera.onMeshTargetChangedObservable.remove(this._onMeshTargetChangedObserver);
  193. }
  194. this._attachedCamera = null;
  195. }
  196. // Framing control
  197. private _animatables = new Array<Animatable>();
  198. private _betaIsAnimating = false;
  199. private _betaTransition: Animation;
  200. private _radiusTransition: Animation;
  201. private _vectorTransition: Animation;
  202. /**
  203. * Targets the given mesh and updates zoom level accordingly.
  204. * @param mesh The mesh to target.
  205. * @param radius Optional. If a cached radius position already exists, overrides default.
  206. * @param framingPositionY Position on mesh to center camera focus where 0 corresponds bottom of its bounding box and 1, the top
  207. * @param focusOnOriginXZ Determines if the camera should focus on 0 in the X and Z axis instead of the mesh
  208. * @param onAnimationEnd Callback triggered at the end of the framing animation
  209. */
  210. public zoomOnMesh(mesh: AbstractMesh, focusOnOriginXZ: boolean = false, onAnimationEnd: Nullable<() => void> = null): void {
  211. mesh.computeWorldMatrix(true);
  212. let boundingBox = mesh.getBoundingInfo().boundingBox;
  213. this.zoomOnBoundingInfo(boundingBox.minimumWorld, boundingBox.maximumWorld, focusOnOriginXZ, onAnimationEnd);
  214. }
  215. /**
  216. * Targets the given mesh with its children and updates zoom level accordingly.
  217. * @param mesh The mesh to target.
  218. * @param radius Optional. If a cached radius position already exists, overrides default.
  219. * @param framingPositionY Position on mesh to center camera focus where 0 corresponds bottom of its bounding box and 1, the top
  220. * @param focusOnOriginXZ Determines if the camera should focus on 0 in the X and Z axis instead of the mesh
  221. * @param onAnimationEnd Callback triggered at the end of the framing animation
  222. */
  223. public zoomOnMeshHierarchy(mesh: AbstractMesh, focusOnOriginXZ: boolean = false, onAnimationEnd: Nullable<() => void> = null): void {
  224. mesh.computeWorldMatrix(true);
  225. let boundingBox = mesh.getHierarchyBoundingVectors(true);
  226. this.zoomOnBoundingInfo(boundingBox.min, boundingBox.max, focusOnOriginXZ, onAnimationEnd);
  227. }
  228. /**
  229. * Targets the given meshes with their children and updates zoom level accordingly.
  230. * @param meshes The mesh to target.
  231. * @param radius Optional. If a cached radius position already exists, overrides default.
  232. * @param framingPositionY Position on mesh to center camera focus where 0 corresponds bottom of its bounding box and 1, the top
  233. * @param focusOnOriginXZ Determines if the camera should focus on 0 in the X and Z axis instead of the mesh
  234. * @param onAnimationEnd Callback triggered at the end of the framing animation
  235. */
  236. public zoomOnMeshesHierarchy(meshes: AbstractMesh[], focusOnOriginXZ: boolean = false, onAnimationEnd: Nullable<() => void> = null): void {
  237. let min = new Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
  238. let max = new Vector3(-Number.MAX_VALUE, -Number.MAX_VALUE, -Number.MAX_VALUE);
  239. for (let i = 0; i < meshes.length; i++) {
  240. let boundingInfo = meshes[i].getHierarchyBoundingVectors(true);
  241. Tools.CheckExtends(boundingInfo.min, min, max);
  242. Tools.CheckExtends(boundingInfo.max, min, max);
  243. }
  244. this.zoomOnBoundingInfo(min, max, focusOnOriginXZ, onAnimationEnd);
  245. }
  246. /**
  247. * Targets the bounding box info defined by its extends and updates zoom level accordingly.
  248. * @param minimumWorld Determines the smaller position of the bounding box extend
  249. * @param maximumWorld Determines the bigger position of the bounding box extend
  250. * @param focusOnOriginXZ Determines if the camera should focus on 0 in the X and Z axis instead of the mesh
  251. * @param onAnimationEnd Callback triggered at the end of the framing animation
  252. */
  253. public zoomOnBoundingInfo(minimumWorld: Vector3, maximumWorld: Vector3, focusOnOriginXZ: boolean = false, onAnimationEnd: Nullable<() => void> = null): void {
  254. let zoomTarget: Vector3;
  255. if (!this._attachedCamera) {
  256. return;
  257. }
  258. // Find target by interpolating from bottom of bounding box in world-space to top via framingPositionY
  259. let bottom = minimumWorld.y;
  260. let top = maximumWorld.y;
  261. let zoomTargetY = bottom + (top - bottom) * this._positionScale;
  262. let radiusWorld = maximumWorld.subtract(minimumWorld).scale(0.5);
  263. if (focusOnOriginXZ) {
  264. zoomTarget = new Vector3(0, zoomTargetY, 0);
  265. } else {
  266. let centerWorld = minimumWorld.add(radiusWorld);
  267. zoomTarget = new Vector3(centerWorld.x, zoomTargetY, centerWorld.z);
  268. }
  269. if (!this._vectorTransition) {
  270. this._vectorTransition = Animation.CreateAnimation("target", Animation.ANIMATIONTYPE_VECTOR3, 60, FramingBehavior.EasingFunction);
  271. }
  272. this._betaIsAnimating = true;
  273. let animatable = Animation.TransitionTo("target", zoomTarget, this._attachedCamera, this._attachedCamera.getScene(), 60, this._vectorTransition, this._framingTime);
  274. if (animatable) {
  275. this._animatables.push(animatable);
  276. }
  277. // sets the radius and lower radius bounds
  278. // Small delta ensures camera is not always at lower zoom limit.
  279. let radius = 0;
  280. if (this._mode === FramingBehavior.FitFrustumSidesMode) {
  281. let position = this._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld);
  282. if (this.autoCorrectCameraLimitsAndSensibility) {
  283. this._attachedCamera.lowerRadiusLimit = radiusWorld.length() + this._attachedCamera.minZ;
  284. }
  285. radius = position;
  286. } else if (this._mode === FramingBehavior.IgnoreBoundsSizeMode) {
  287. radius = this._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld);
  288. if (this.autoCorrectCameraLimitsAndSensibility && this._attachedCamera.lowerRadiusLimit === null) {
  289. this._attachedCamera.lowerRadiusLimit = this._attachedCamera.minZ;
  290. }
  291. }
  292. // Set sensibilities
  293. if (this.autoCorrectCameraLimitsAndSensibility) {
  294. const extend = maximumWorld.subtract(minimumWorld).length();
  295. this._attachedCamera.panningSensibility = 5000 / extend;
  296. this._attachedCamera.wheelPrecision = 100 / radius;
  297. }
  298. // transition to new radius
  299. if (!this._radiusTransition) {
  300. this._radiusTransition = Animation.CreateAnimation("radius", Animation.ANIMATIONTYPE_FLOAT, 60, FramingBehavior.EasingFunction);
  301. }
  302. animatable = Animation.TransitionTo("radius", radius, this._attachedCamera, this._attachedCamera.getScene(),
  303. 60, this._radiusTransition, this._framingTime, () => {
  304. this.stopAllAnimations();
  305. if (onAnimationEnd) {
  306. onAnimationEnd();
  307. }
  308. if (this._attachedCamera) {
  309. this._attachedCamera.storeState();
  310. }
  311. });
  312. if (animatable) {
  313. this._animatables.push(animatable);
  314. }
  315. }
  316. /**
  317. * Calculates the lowest radius for the camera based on the bounding box of the mesh.
  318. * @param mesh The mesh on which to base the calculation. mesh boundingInfo used to estimate necessary
  319. * frustum width.
  320. * @return The minimum distance from the primary mesh's center point at which the camera must be kept in order
  321. * to fully enclose the mesh in the viewing frustum.
  322. */
  323. protected _calculateLowerRadiusFromModelBoundingSphere(minimumWorld: Vector3, maximumWorld: Vector3): number {
  324. let size = maximumWorld.subtract(minimumWorld);
  325. let boxVectorGlobalDiagonal = size.length();
  326. let frustumSlope: Vector2 = this._getFrustumSlope();
  327. // Formula for setting distance
  328. // (Good explanation: http://stackoverflow.com/questions/2866350/move-camera-to-fit-3d-scene)
  329. let radiusWithoutFraming = boxVectorGlobalDiagonal * 0.5;
  330. // Horizon distance
  331. let radius = radiusWithoutFraming * this._radiusScale;
  332. let distanceForHorizontalFrustum = radius * Math.sqrt(1.0 + 1.0 / (frustumSlope.x * frustumSlope.x));
  333. let distanceForVerticalFrustum = radius * Math.sqrt(1.0 + 1.0 / (frustumSlope.y * frustumSlope.y));
  334. let distance = Math.max(distanceForHorizontalFrustum, distanceForVerticalFrustum);
  335. let camera = this._attachedCamera;
  336. if (!camera) {
  337. return 0;
  338. }
  339. if (camera.lowerRadiusLimit && this._mode === FramingBehavior.IgnoreBoundsSizeMode) {
  340. // Don't exceed the requested limit
  341. distance = distance < camera.lowerRadiusLimit ? camera.lowerRadiusLimit : distance;
  342. }
  343. // Don't exceed the upper radius limit
  344. if (camera.upperRadiusLimit) {
  345. distance = distance > camera.upperRadiusLimit ? camera.upperRadiusLimit : distance;
  346. }
  347. return distance;
  348. }
  349. /**
  350. * Keeps the camera above the ground plane. If the user pulls the camera below the ground plane, the camera
  351. * is automatically returned to its default position (expected to be above ground plane).
  352. */
  353. private _maintainCameraAboveGround(): void {
  354. if (this._elevationReturnTime < 0) {
  355. return;
  356. }
  357. let timeSinceInteraction = Tools.Now - this._lastInteractionTime;
  358. let defaultBeta = Math.PI * 0.5 - this._defaultElevation;
  359. let limitBeta = Math.PI * 0.5;
  360. // Bring the camera back up if below the ground plane
  361. if (this._attachedCamera && !this._betaIsAnimating && this._attachedCamera.beta > limitBeta && timeSinceInteraction >= this._elevationReturnWaitTime) {
  362. this._betaIsAnimating = true;
  363. //Transition to new position
  364. this.stopAllAnimations();
  365. if (!this._betaTransition) {
  366. this._betaTransition = Animation.CreateAnimation("beta", Animation.ANIMATIONTYPE_FLOAT, 60, FramingBehavior.EasingFunction);
  367. }
  368. let animatabe = Animation.TransitionTo("beta", defaultBeta, this._attachedCamera, this._attachedCamera.getScene(), 60,
  369. this._betaTransition, this._elevationReturnTime,
  370. () => {
  371. this._clearAnimationLocks();
  372. this.stopAllAnimations();
  373. });
  374. if (animatabe) {
  375. this._animatables.push(animatabe);
  376. }
  377. }
  378. }
  379. /**
  380. * Returns the frustum slope based on the canvas ratio and camera FOV
  381. * @returns The frustum slope represented as a Vector2 with X and Y slopes
  382. */
  383. private _getFrustumSlope(): Vector2 {
  384. // Calculate the viewport ratio
  385. // Aspect Ratio is Height/Width.
  386. let camera = this._attachedCamera;
  387. if (!camera) {
  388. return Vector2.Zero();
  389. }
  390. let engine = camera.getScene().getEngine();
  391. var aspectRatio = engine.getAspectRatio(camera);
  392. // Camera FOV is the vertical field of view (top-bottom) in radians.
  393. // Slope of the frustum top/bottom planes in view space, relative to the forward vector.
  394. var frustumSlopeY = Math.tan(camera.fov / 2);
  395. // Slope of the frustum left/right planes in view space, relative to the forward vector.
  396. // Provides the amount that one side (e.g. left) of the frustum gets wider for every unit
  397. // along the forward vector.
  398. var frustumSlopeX = frustumSlopeY * aspectRatio;
  399. return new Vector2(frustumSlopeX, frustumSlopeY);
  400. }
  401. /**
  402. * Removes all animation locks. Allows new animations to be added to any of the arcCamera properties.
  403. */
  404. private _clearAnimationLocks(): void {
  405. this._betaIsAnimating = false;
  406. }
  407. /**
  408. * Applies any current user interaction to the camera. Takes into account maximum alpha rotation.
  409. */
  410. private _applyUserInteraction(): void {
  411. if (this.isUserIsMoving) {
  412. this._lastInteractionTime = Tools.Now;
  413. this.stopAllAnimations();
  414. this._clearAnimationLocks();
  415. }
  416. }
  417. /**
  418. * Stops and removes all animations that have been applied to the camera
  419. */
  420. public stopAllAnimations(): void {
  421. if (this._attachedCamera) {
  422. this._attachedCamera.animations = [];
  423. }
  424. while (this._animatables.length) {
  425. if (this._animatables[0]) {
  426. this._animatables[0].onAnimationEnd = null;
  427. this._animatables[0].stop();
  428. }
  429. this._animatables.shift();
  430. }
  431. }
  432. /**
  433. * Gets a value indicating if the user is moving the camera
  434. */
  435. public get isUserIsMoving(): boolean {
  436. if (!this._attachedCamera) {
  437. return false;
  438. }
  439. return this._attachedCamera.inertialAlphaOffset !== 0 ||
  440. this._attachedCamera.inertialBetaOffset !== 0 ||
  441. this._attachedCamera.inertialRadiusOffset !== 0 ||
  442. this._attachedCamera.inertialPanningX !== 0 ||
  443. this._attachedCamera.inertialPanningY !== 0 ||
  444. this._isPointerDown;
  445. }
  446. // Statics
  447. /**
  448. * The camera can move all the way towards the mesh.
  449. */
  450. public static IgnoreBoundsSizeMode = 0;
  451. /**
  452. * The camera is not allowed to zoom closer to the mesh than the point at which the adjusted bounding sphere touches the frustum sides
  453. */
  454. public static FitFrustumSidesMode = 1;
  455. }
  456. }