diff --git a/packages/dev/core/src/Behaviors/Cameras/framingBehavior.ts b/packages/dev/core/src/Behaviors/Cameras/framingBehavior.ts index e467d94aa35..17f7eb9d07a 100644 --- a/packages/dev/core/src/Behaviors/Cameras/framingBehavior.ts +++ b/packages/dev/core/src/Behaviors/Cameras/framingBehavior.ts @@ -10,7 +10,7 @@ import { PointerEventTypes } from "../../Events/pointerEvents"; import { PrecisionDate } from "../../Misc/precisionDate"; import type { AbstractMesh } from "../../Meshes/abstractMesh"; -import { Vector3, Vector2 } from "../../Maths/math.vector"; +import { Vector3 } from "../../Maths/math.vector"; import type { Animatable } from "../../Animations/animatable"; import { Animation } from "../../Animations/animation"; @@ -395,25 +395,13 @@ export class FramingBehavior implements Behavior { * to fully enclose the mesh in the viewing frustum. */ protected _calculateLowerRadiusFromModelBoundingSphere(minimumWorld: Vector3, maximumWorld: Vector3): number { - const size = maximumWorld.subtract(minimumWorld); - const boxVectorGlobalDiagonal = size.length(); - const frustumSlope: Vector2 = this._getFrustumSlope(); - - // Formula for setting distance - // (Good explanation: http://stackoverflow.com/questions/2866350/move-camera-to-fit-3d-scene) - const radiusWithoutFraming = boxVectorGlobalDiagonal * 0.5; - - // Horizon distance - const radius = radiusWithoutFraming * this._radiusScale; - const distanceForHorizontalFrustum = radius * Math.sqrt(1.0 + 1.0 / (frustumSlope.x * frustumSlope.x)); - const distanceForVerticalFrustum = radius * Math.sqrt(1.0 + 1.0 / (frustumSlope.y * frustumSlope.y)); - let distance = Math.max(distanceForHorizontalFrustum, distanceForVerticalFrustum); const camera = this._attachedCamera; if (!camera) { return 0; } + let distance = camera._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld, this._radiusScale); if (camera.lowerRadiusLimit && this._mode === FramingBehavior.IgnoreBoundsSizeMode) { // Don't exceed the requested limit distance = distance < camera.lowerRadiusLimit ? camera.lowerRadiusLimit : distance; @@ -471,34 +459,6 @@ export class FramingBehavior implements Behavior { } } - /** - * Returns the frustum slope based on the canvas ratio and camera FOV - * @returns The frustum slope represented as a Vector2 with X and Y slopes - */ - private _getFrustumSlope(): Vector2 { - // Calculate the viewport ratio - // Aspect Ratio is Height/Width. - const camera = this._attachedCamera; - - if (!camera) { - return Vector2.Zero(); - } - - const engine = camera.getScene().getEngine(); - const aspectRatio = engine.getAspectRatio(camera); - - // Camera FOV is the vertical field of view (top-bottom) in radians. - // Slope of the frustum top/bottom planes in view space, relative to the forward vector. - const frustumSlopeY = Math.tan(camera.fov / 2); - - // Slope of the frustum left/right planes in view space, relative to the forward vector. - // Provides the amount that one side (e.g. left) of the frustum gets wider for every unit - // along the forward vector. - const frustumSlopeX = frustumSlopeY * aspectRatio; - - return new Vector2(frustumSlopeX, frustumSlopeY); - } - /** * Removes all animation locks. Allows new animations to be added to any of the arcCamera properties. */ diff --git a/packages/dev/core/src/Cameras/arcRotateCamera.ts b/packages/dev/core/src/Cameras/arcRotateCamera.ts index 8b1ced4aebf..479b849b98e 100644 --- a/packages/dev/core/src/Cameras/arcRotateCamera.ts +++ b/packages/dev/core/src/Cameras/arcRotateCamera.ts @@ -1213,8 +1213,10 @@ export class ArcRotateCamera extends TargetCamera { meshes = meshes || this.getScene().meshes; const minMaxVector = Mesh.MinMax(meshes); - const distance = Vector3.Distance(minMaxVector.min, minMaxVector.max); + let distance = this._calculateLowerRadiusFromModelBoundingSphere(minMaxVector.min, minMaxVector.max); + // If there are defined limits, we need to take them into account + distance = Math.max(Math.min(distance, this.upperRadiusLimit || Number.MAX_VALUE), this.lowerRadiusLimit || 0); this.radius = distance * this.zoomOnFactor; this.focusOn({ min: minMaxVector.min, max: minMaxVector.max, distance: distance }, doNotUpdateMaxZ); @@ -1310,6 +1312,29 @@ export class ArcRotateCamera extends TargetCamera { super._updateRigCameras(); } + /** + * @internal + */ + public _calculateLowerRadiusFromModelBoundingSphere(minimumWorld: Vector3, maximumWorld: Vector3, radiusScale: number = 1): number { + const boxVectorGlobalDiagonal = Vector3.Distance(minimumWorld, maximumWorld); + + // Get aspect ratio in order to calculate frustum slope + const engine = this.getScene().getEngine(); + const aspectRatio = engine.getAspectRatio(this); + const frustumSlopeY = Math.tan(this.fov / 2); + const frustumSlopeX = frustumSlopeY * aspectRatio; + + // Formula for setting distance + // (Good explanation: http://stackoverflow.com/questions/2866350/move-camera-to-fit-3d-scene) + const radiusWithoutFraming = boxVectorGlobalDiagonal * 0.5; + + // Horizon distance + const radius = radiusWithoutFraming * radiusScale; + const distanceForHorizontalFrustum = radius * Math.sqrt(1.0 + 1.0 / (frustumSlopeX * frustumSlopeX)); + const distanceForVerticalFrustum = radius * Math.sqrt(1.0 + 1.0 / (frustumSlopeY * frustumSlopeY)); + return Math.max(distanceForHorizontalFrustum, distanceForVerticalFrustum); + } + /** * Destroy the camera and release the current resources hold by it. */ diff --git a/packages/dev/core/test/unit/Cameras/babylon.arcRotateCameraInputs.test.ts b/packages/dev/core/test/unit/Cameras/babylon.arcRotateCameraInputs.test.ts index ce3cdecab01..9752434725d 100644 --- a/packages/dev/core/test/unit/Cameras/babylon.arcRotateCameraInputs.test.ts +++ b/packages/dev/core/test/unit/Cameras/babylon.arcRotateCameraInputs.test.ts @@ -9,6 +9,9 @@ import { Vector3 } from "core/Maths/math.vector"; import { Scene } from "core/scene"; import type { Nullable } from "core/types"; import { TestDeviceInputSystem } from "../DeviceInput/testDeviceInputSystem"; +import { MeshBuilder } from "core/Meshes/meshBuilder"; +import { Frustum } from "core/Maths"; +import { StandardMaterial } from "core/Materials"; describe("ArcRotateCameraMouseInput", () => { let engine: Nullable = null; @@ -119,7 +122,7 @@ describe("ArcRotateCameraMouseInput", () => { scene?.onPointerObservable.notifyObservers(movePI1); scene?.onPointerObservable.notifyObservers(movePI2); scene?.render(); - expect (camera!.radius).toBeGreaterThan(radius); + expect(camera!.radius).toBeGreaterThan(radius); radius = camera!.radius; @@ -155,4 +158,54 @@ describe("ArcRotateCameraMouseInput", () => { expect(camera!.alpha).toBeGreaterThan(alpha); expect(camera!.beta).toBeGreaterThan(beta); }); + + it("correctly zooms when zoomOn is called", () => { + let outOfBoundsPoints = 0; + let inBoundsPoints = 0; + + if (camera && scene && StandardMaterial) { + // Create box to check zoomOn against + const box = MeshBuilder.CreateBox("box", { height: 1, width: 2, depth: 1 }, scene); + // Set angles such that the box's mix/max points are not technically + // the farthest points in screen/camera space + camera.alpha = Math.PI / 3; + camera.beta = Math.PI / 2.5; + camera.radius = 0.01; + box.position = new Vector3(0, 0.5, 0); + scene.render(); + + // Get frustum planes from camera transformation matrix + let transformMatrix = camera.getTransformationMatrix(); + let frustumPlanes = Frustum.GetPlanes(transformMatrix); + + // Get all bounding box points and check if they are in the frustum + // both before and after zoomOn + const pointsToCheck = box.getBoundingInfo().boundingBox.vectorsWorld; + + // Before zoomOn + for (const point of pointsToCheck) { + if (!Frustum.IsPointInFrustum(point, frustumPlanes)) { + outOfBoundsPoints++; + } + } + + scene.render(); + camera.zoomOn([box]); + scene.render(); + + // Update frustum planes and transformation matrix + transformMatrix = camera.getTransformationMatrix(); + frustumPlanes = Frustum.GetPlanes(transformMatrix); + + // After zoomOn + for (const point of pointsToCheck) { + if (Frustum.IsPointInFrustum(point, frustumPlanes)) { + inBoundsPoints++; + } + } + } + + expect(outOfBoundsPoints).toEqual(8); + expect(inBoundsPoints).toEqual(8); + }); });