Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 67 additions & 11 deletions packages/dev/core/src/Behaviors/Cameras/framingBehavior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import { Vector3 } from "../../Maths/math.vector";
import type { Animatable } from "../../Animations/animatable.core";
import { Animation } from "../../Animations/animation";

// Add this exported union type so callers and code get type-safety for the mode param of the ZoonOnBoundingInfo method
export type BoundingInfoMode = "sphere" | "box";

/**
* The framing behavior (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.
* @see https://doc.babylonjs.com/features/featuresDeepDive/behaviors/cameraBehaviors#framing-behavior
Expand Down Expand Up @@ -310,9 +313,18 @@ export class FramingBehavior implements Behavior<ArcRotateCamera> {
* @param maximumWorld Determines the bigger position of the bounding box extend
* @param focusOnOriginXZ Determines if the camera should focus on 0 in the X and Z axis instead of the mesh
* @param onAnimationEnd Callback triggered at the end of the framing animation
* @param mode Defines the method used to compute the ideal radius ("sphere" or "box")
* @param radiusScaling Defines a scaling factor to apply to the computed radius
* @returns true if the zoom was done
*/
public zoomOnBoundingInfo(minimumWorld: Vector3, maximumWorld: Vector3, focusOnOriginXZ: boolean = false, onAnimationEnd: Nullable<() => void> = null): boolean {
public zoomOnBoundingInfo(
minimumWorld: Vector3,
maximumWorld: Vector3,
focusOnOriginXZ: boolean = false,
onAnimationEnd: Nullable<() => void> = null,
mode: BoundingInfoMode = "sphere",
radiusScaling: number = this._radiusScale
): boolean {
let zoomTarget: Vector3;

if (!this._attachedCamera) {
Expand Down Expand Up @@ -349,16 +361,32 @@ export class FramingBehavior implements Behavior<ArcRotateCamera> {
// sets the radius and lower radius bounds
// Small delta ensures camera is not always at lower zoom limit.
let radius = 0;
let position = 0;
if (this._mode === FramingBehavior.FitFrustumSidesMode) {
const position = this._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld);
if (this.autoCorrectCameraLimitsAndSensibility) {
this._attachedCamera.lowerRadiusLimit = radiusWorld.length() + this._attachedCamera.minZ;
if (mode === "sphere") {
position = this._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld, radiusScaling);
if (this.autoCorrectCameraLimitsAndSensibility) {
this._attachedCamera.lowerRadiusLimit = radiusWorld.length() + this._attachedCamera.minZ;
}
radius = position;
} else if (mode === "box") {
position = this._attachedCamera._calculateLowerRadiusFromModelBoundingBox(minimumWorld, maximumWorld, radiusScaling);
if (this.autoCorrectCameraLimitsAndSensibility) {
this._attachedCamera.lowerRadiusLimit = radiusWorld.length() + this._attachedCamera.minZ;
}
radius = position;
}
radius = position;
} else if (this._mode === FramingBehavior.IgnoreBoundsSizeMode) {
radius = this._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld);
if (this.autoCorrectCameraLimitsAndSensibility && this._attachedCamera.lowerRadiusLimit === null) {
this._attachedCamera.lowerRadiusLimit = this._attachedCamera.minZ;
if (mode === "sphere") {
radius = this._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld, radiusScaling);
if (this.autoCorrectCameraLimitsAndSensibility && this._attachedCamera.lowerRadiusLimit === null) {
this._attachedCamera.lowerRadiusLimit = this._attachedCamera.minZ;
}
} else if (mode === "box") {
radius = this._calculateLowerRadiusFromModelBoundingBox(minimumWorld, maximumWorld, radiusScaling);
if (this.autoCorrectCameraLimitsAndSensibility && this._attachedCamera.lowerRadiusLimit === null) {
this._attachedCamera.lowerRadiusLimit = this._attachedCamera.minZ;
}
}
}

Expand Down Expand Up @@ -393,20 +421,48 @@ export class FramingBehavior implements Behavior<ArcRotateCamera> {
}

/**
* Calculates the lowest radius for the camera based on the bounding box of the mesh.
* Calculates the lowest radius for the camera based on the bounding sphere of the mesh.
* @param minimumWorld
* @param maximumWorld
* @param radiusScale
* @returns The minimum distance from the primary mesh's center point at which the camera must be kept in order
* to fully enclose the mesh in the viewing frustum.
*/
protected _calculateLowerRadiusFromModelBoundingSphere(minimumWorld: Vector3, maximumWorld: Vector3): number {
protected _calculateLowerRadiusFromModelBoundingSphere(minimumWorld: Vector3, maximumWorld: Vector3, radiusScale: number = this._radiusScale): number {
const camera = this._attachedCamera;

if (!camera) {
return 0;
}
let distance = camera._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld, radiusScale);
if (camera.lowerRadiusLimit && this._mode === FramingBehavior.IgnoreBoundsSizeMode) {
// Don't exceed the requested limit
distance = distance < camera.lowerRadiusLimit ? camera.lowerRadiusLimit : distance;
}

// Don't exceed the upper radius limit
if (camera.upperRadiusLimit) {
distance = distance > camera.upperRadiusLimit ? camera.upperRadiusLimit : distance;
}

let distance = camera._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld, this._radiusScale);
return distance;
}

/**
* Calculates the lowest radius for the camera based on the bounding box of the mesh.
* @param minimumWorld
* @param maximumWorld
* @param radiusScale
* @returns The minimum distance from the primary mesh's center point at which the camera must be kept in order
* to fully enclose the mesh in the viewing frustum.
*/
protected _calculateLowerRadiusFromModelBoundingBox(minimumWorld: Vector3, maximumWorld: Vector3, radiusScale: number = this._radiusScale): number {
const camera = this._attachedCamera;

if (!camera) {
return 0;
}
let distance = camera._calculateLowerRadiusFromModelBoundingBox(minimumWorld, maximumWorld, radiusScale);
if (camera.lowerRadiusLimit && this._mode === FramingBehavior.IgnoreBoundsSizeMode) {
// Don't exceed the requested limit
distance = distance < camera.lowerRadiusLimit ? camera.lowerRadiusLimit : distance;
Expand Down
56 changes: 56 additions & 0 deletions packages/dev/core/src/Cameras/arcRotateCamera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1578,8 +1578,64 @@ export class ArcRotateCamera extends TargetCamera {
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));

// Setting orthographic extents
const height = (maximumWorld.y - minimumWorld.y) * 0.5;
const width = (maximumWorld.x - minimumWorld.x) * 0.5;

if (this.mode === Camera.ORTHOGRAPHIC_CAMERA) {
if (aspectRatio < width / height) {
this.orthoRight = width * radiusScale;
this.orthoLeft = -this.orthoRight;
this.orthoTop = this.orthoRight / aspectRatio;
this.orthoBottom = this.orthoLeft / aspectRatio;
} else {
this.orthoTop = height * radiusScale;
this.orthoBottom = -this.orthoTop;
this.orthoRight = this.orthoTop * aspectRatio;
this.orthoLeft = -this.orthoRight;
}
}
return Math.max(distanceForHorizontalFrustum, distanceForVerticalFrustum);
}
/**
* @internal
*/
public _calculateLowerRadiusFromModelBoundingBox(minimumWorld: Vector3, maximumWorld: Vector3, radiusScale: number = 1): number {
// 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;
const oldRadius = this.radius;
let distance = oldRadius;

// Formula for setting distance
let distanceForHorizontalFrustum: number = 0;
let distanceForVerticalFrustum: number = 0;
const height = (maximumWorld.y - minimumWorld.y) * 0.5;
const width = (maximumWorld.x - minimumWorld.x) * 0.5;
const depth = (maximumWorld.z - minimumWorld.z) * 0.5;
// Setting the distance according to the bounding box (centring the first face of the bounding box)
distanceForHorizontalFrustum = (radiusScale * width) / frustumSlopeX + depth;
distanceForVerticalFrustum = (radiusScale * height) / frustumSlopeY + depth;
distance = Math.max(distanceForHorizontalFrustum, distanceForVerticalFrustum);
//Adding a check for orthographic Camera
if (this.mode === Camera.ORTHOGRAPHIC_CAMERA) {
if (aspectRatio < width / height) {
this.orthoRight = width * radiusScale;
this.orthoLeft = -this.orthoRight;
this.orthoTop = this.orthoRight / aspectRatio;
this.orthoBottom = this.orthoLeft / aspectRatio;
} else {
this.orthoTop = height * radiusScale;
this.orthoBottom = -this.orthoTop;
this.orthoRight = this.orthoTop * aspectRatio;
this.orthoLeft = -this.orthoRight;
}
}
return distance;
}

/**
* Destroy the camera and release the current resources hold by it.
Expand Down