Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ArcRotateCamera: Modified zoomOn to use same logic as FramingBehavior #14387

Merged
merged 5 commits into from Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 2 additions & 28 deletions packages/dev/core/src/Behaviors/Cameras/framingBehavior.ts
Expand Up @@ -395,25 +395,13 @@ export class FramingBehavior implements Behavior<ArcRotateCamera> {
* 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._getFrustumSlope(), this._radiusScale);
if (camera.lowerRadiusLimit && this._mode === FramingBehavior.IgnoreBoundsSizeMode) {
// Don't exceed the requested limit
distance = distance < camera.lowerRadiusLimit ? camera.lowerRadiusLimit : distance;
Expand Down Expand Up @@ -476,27 +464,13 @@ export class FramingBehavior implements Behavior<ArcRotateCamera> {
* @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);
return camera._getFrustumSlope();
}

/**
Expand Down
49 changes: 48 additions & 1 deletion packages/dev/core/src/Cameras/arcRotateCamera.ts
Expand Up @@ -1207,8 +1207,11 @@ 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(distance, this.lowerRadiusLimit ?? 0);
distance = Math.min(distance, this.upperRadiusLimit ?? Number.MAX_VALUE);
PolygonalSun marked this conversation as resolved.
Show resolved Hide resolved
this.radius = distance * this.zoomOnFactor;

this.focusOn({ min: minMaxVector.min, max: minMaxVector.max, distance: distance }, doNotUpdateMaxZ);
Expand Down Expand Up @@ -1304,6 +1307,50 @@ export class ArcRotateCamera extends TargetCamera {
super._updateRigCameras();
}

/**
* @internal
*/
public _calculateLowerRadiusFromModelBoundingSphere(
minimumWorld: Vector3,
maximumWorld: Vector3,
frustumSlope: Vector2 = this._getFrustumSlope(),
radiusScale: number = 1
): number {
const size = maximumWorld.subtract(minimumWorld);
PolygonalSun marked this conversation as resolved.
Show resolved Hide resolved
const boxVectorGlobalDiagonal = size.length();

// 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 / (frustumSlope.x * frustumSlope.x));
const distanceForVerticalFrustum = radius * Math.sqrt(1.0 + 1.0 / (frustumSlope.y * frustumSlope.y));
return Math.max(distanceForHorizontalFrustum, distanceForVerticalFrustum);
}

/**
* @internal
*/
public _getFrustumSlope(): Vector2 {
PolygonalSun marked this conversation as resolved.
Show resolved Hide resolved
// Calculate the viewport ratio
// Aspect Ratio is Height/Width.
const engine = this.getScene().getEngine();
const aspectRatio = engine.getAspectRatio(this);

// 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(this.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);
}

/**
* Destroy the camera and release the current resources hold by it.
*/
Expand Down
Expand Up @@ -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<NullEngine> = null;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
});
});