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
42 changes: 35 additions & 7 deletions packages/dev/core/src/Behaviors/Cameras/framingBehavior.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Behavior } from "../../Behaviors/behavior";
import type { Camera } from "../../Cameras/camera";
import type { ArcRotateCamera } from "../../Cameras/arcRotateCamera";
import type { ArcRotateCamera, BoundingInfoMode } from "../../Cameras/arcRotateCamera";
import { ExponentialEase, EasingFunction } from "../../Animations/easing";
import type { Observer } from "../../Misc/observable";
import { Observable } from "../../Misc/observable";
Expand Down Expand Up @@ -310,9 +310,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 lower radius ("sphere" or "box"). Default is 'sphere. Pass explicit mode if you want orthographic framing
* @param radiusScaling Defines a scaling factor to apply to the computed radius. Defaults to this._radiusScale
* @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,
radiusScaling?: number
): boolean {
let zoomTarget: Vector3;

if (!this._attachedCamera) {
Expand Down Expand Up @@ -350,13 +359,12 @@ export class FramingBehavior implements Behavior<ArcRotateCamera> {
// Small delta ensures camera is not always at lower zoom limit.
let radius = 0;
if (this._mode === FramingBehavior.FitFrustumSidesMode) {
const position = this._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld);
radius = this._calculateLowerRadiusFromModelBoundingInfo(minimumWorld, maximumWorld, mode, radiusScaling);
if (this.autoCorrectCameraLimitsAndSensibility) {
this._attachedCamera.lowerRadiusLimit = radiusWorld.length() + this._attachedCamera.minZ;
}
radius = position;
} else if (this._mode === FramingBehavior.IgnoreBoundsSizeMode) {
radius = this._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld);
radius = this._calculateLowerRadiusFromModelBoundingInfo(minimumWorld, maximumWorld, mode, radiusScaling);
if (this.autoCorrectCameraLimitsAndSensibility && this._attachedCamera.lowerRadiusLimit === null) {
this._attachedCamera.lowerRadiusLimit = this._attachedCamera.minZ;
}
Expand Down Expand Up @@ -393,20 +401,40 @@ 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
* @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 {
return this._calculateLowerRadiusFromModelBoundingInfo(minimumWorld, maximumWorld);
}

/**
* Calculates the lowest radius for the camera based on the bounding sphere or box of the mesh.
* @param minimumWorld
* @param maximumWorld
* @param mode defines whether we calculate lowerradius from bounding sphere or box. If undefined, defaults to old spherical behavior. If defined, uses cameras boundingInfo fn
* @param radiusScale Defines a scaling factor to apply to the computed radius. Defaults to this._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 _calculateLowerRadiusFromModelBoundingInfo(minimumWorld: Vector3, maximumWorld: Vector3, mode?: BoundingInfoMode, radiusScale?: number): number {
const camera = this._attachedCamera;

if (!camera) {
return 0;
}

let distance = camera._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld, this._radiusScale);
let distance = 0;
if (mode === undefined) {
// For backcompat, call directly into the boundingsphere function if mode is undefined
distance = camera._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld, radiusScale ?? this._radiusScale);
} else {
// If mode is explicitly defined, use new camera function which includes orthographic framing
distance = camera._calculateLowerRadiusFromModelBoundingInfo(minimumWorld, maximumWorld, mode, radiusScale ?? this._radiusScale);
}
if (camera.lowerRadiusLimit && this._mode === FramingBehavior.IgnoreBoundsSizeMode) {
// Don't exceed the requested limit
distance = distance < camera.lowerRadiusLimit ? camera.lowerRadiusLimit : distance;
Expand Down
42 changes: 42 additions & 0 deletions packages/dev/core/src/Cameras/arcRotateCamera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Node.AddNodeConstructor("ArcRotateCamera", (name, scene) => {
return () => new ArcRotateCamera(name, 0, 0, 1.0, Vector3.Zero(), scene);
});

// Used to indicate boundingInfo for the calculateLowerRadius methods
export type BoundingInfoMode = "sphere" | "box";

/**
* Computes the alpha angle based on the source position and the target position.
* @param offset The directional offset between the source position and the target position
Expand Down Expand Up @@ -1581,6 +1584,45 @@ export class ArcRotateCamera extends TargetCamera {
return Math.max(distanceForHorizontalFrustum, distanceForVerticalFrustum);
}

/**
* @internal
* This expands functionality beyond what _calculateLowerRadiusFromModelBoundingSphere does by
* 1. Offering boundingBox mode which calculates distance according to the bounding box
* 2. Setting orthographic extents on the class
*/
public _calculateLowerRadiusFromModelBoundingInfo(minimumWorld: Vector3, maximumWorld: Vector3, mode: BoundingInfoMode = "sphere", radiusScale: number = 1): number {
// Setting orthographic extents -- this is done regardless of mode
const height = (maximumWorld.y - minimumWorld.y) * 0.5;
const width = (maximumWorld.x - minimumWorld.x) * 0.5;
const depth = (maximumWorld.z - minimumWorld.z) * 0.5;
const aspectRatio = this.getScene().getEngine().getAspectRatio(this);

if (this.mode === Camera.ORTHOGRAPHIC_CAMERA) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PietroFurlan in your PR the logic here is slightly different - can you confirm which math is correct? do we want the same ortho formula regardless of sphere/box, or should they diffeR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@PietroFurlan closign PR for now as it has been open a while, lets discuss the correct approach via these comments and once aligned i can re-open the PR

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understood correctly: with an orthographic camera there’s no concept of radius or FOV or else, so the sphere/box mode shouldn’t affect the frustum math. Behavior should be identical in ortho mode. In your PR the ortho option existed only for the box mode; by changing the math only there, I introduced the inconsistency—my bad.

In ortho mode, the math is the same for box and sphere, and should be:

this.orthoTop = height * radiusScale;
this.orthoBottom = -this.orthoTop;
this.orthoRight = this.orthoTop * aspectRatio;
this.orthoLeft = -this.orthoRight;

if (aspectRatio < width / height) {
this.orthoRight = width * radiusScale;
this.orthoLeft = -this.orthoRight;
this.orthoTop = this.orthoRight / aspectRatio;
this.orthoBottom = this.orthoLeft / aspectRatio;
} else {
this.orthoRight = height * aspectRatio * radiusScale;
this.orthoLeft = -this.orthoRight * radiusScale;
this.orthoTop = height;
this.orthoBottom = -this.orthoTop;
Comment on lines +1607 to +1610

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is an error on this formula when passing a radiusScale != 1.
In my last commit you find the correct one:

Suggested change
this.orthoRight = height * aspectRatio * radiusScale;
this.orthoLeft = -this.orthoRight * radiusScale;
this.orthoTop = height;
this.orthoBottom = -this.orthoTop;
this.orthoTop = height * radiusScale;
this.orthoBottom = -this.orthoTop;
this.orthoRight = this.orthoTop * aspectRatio;
this.orthoLeft = -this.orthoRight;

}
}

if (mode === "box") {
// Formula for setting distance according to the bounding box (centring the first face of the bounding box)
const frustumSlopeY = Math.tan(this.fov / 2);
const frustumSlopeX = frustumSlopeY * aspectRatio;
const distanceForHorizontalFrustum = (radiusScale * width) / frustumSlopeX + depth;
const distanceForVerticalFrustum = (radiusScale * height) / frustumSlopeY + depth;
return Math.max(distanceForHorizontalFrustum, distanceForVerticalFrustum);
} else {
return this._calculateLowerRadiusFromModelBoundingSphere(minimumWorld, maximumWorld, radiusScale);
}
}

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