Fit projected/'perceived' bounds of mesh into viewport

Hi all!

This problem is driving me crazy:
I have a scene with an Orthographic camera setup so the scene looks isometric.
I want to be able to fit the contents of the scene to the viewport by only setting the camera zoom, so the isometry is always maintained.

The problem as you see in the attached picture is I can’t use the bbox of the mesh directly and I need to calculate somehow that green rectangle that represents the actual area I want to fit in the viewport.

Someone has a clue on how to do that?

Thx!

the fitToBox method in particular.

Thx for your response @drcmda, but I’m already using camera-controls to try to perform the fitToBox, the problem is it rotates the camera to also make the bbox face the camera so I loose my isometric perspective :cry:.

Here’s a video that shows why I can’t just use fitToBox with the object bbox:

That’s why I need to get somehow the green bbox I drew in my picture, so I can then pass it to the fitToBox method of camera-controls, and that should work wonders.

I’ve even tried to understand and mess with the implementation of fitToBox but the little I understand about it seems that it strongly relies on performing the rotation in a first step before fitting… And in general the implementation is waaay too hard for me to be able to modify safely.

My only intuition as of now is that some transformation/projection should be performed of the mesh bbox (red in my picture) to be able to achieve the green bbox that is facing the camera (and is ‘parallel’ to the viewport axes).

i see, i assumed it would do that, i wonder why it has to turn. zoom to fit is very complicated (if it has to animate, support controls and all types of cameras) and there isn’t much out there. i would probably contact yomotsu and ask if i was you.

you use vanilla right? because in react you could still try this:

drei/resize GitHub - pmndrs/drei: 🥉 useful helpers for react-three-fiber
drei/bounds GitHub - pmndrs/drei: 🥉 useful helpers for react-three-fiber

1 Like

Thx a lot @drcmda!

Yes, I use vanilla, but the links you shared seem pretty interesting, specially the drei/bounds one.

Checking the implementation of the fit method it seems it does that transformation of the object bbox to ‘camera space’ I had the intuition it was the way to go:

If it’s ok I may try to use it as a reference to try to implement my own fit method, and share it here back again if I manage to make it work.

of course. if you like you could also try to add it to GitHub - pmndrs/drei-vanilla: 🍦 drei-inspired helpers for threejs so that it can be useful to wider audience.

Here’s one that does it based on all geometry in the scene.
It’s probably slow, but should be accurate.

https://unique-tin-pentaceratops.glitch.me

1 Like

Thx to your help @drcmda and @manthrax I finally managed to make it work for me (OrthographicCamera controlled by yomotsu camera-controls), here it is for future viewers:

// Get the box we want to fit
const box = new THREE.Box3().makeEmpty();
box.setFromObject(mesh);

// Get the center to position later
const center = new THREE.Vector3();
box.getCenter(center);

// Get the corners of the Box3
const corners = [
	new THREE.Vector3(box.min.x, box.max.y, box.min.z),
	new THREE.Vector3(box.max.x, box.max.y, box.min.z),
	new THREE.Vector3(box.min.x, box.max.y, box.max.z),
	new THREE.Vector3(box.max.x, box.max.y, box.max.z)
];

// Now transform the box corners to screen space coords
const screenCoords = [];

for (let i = 0; i < corners.length; i++) {
	// Clone the corner vector
	const vector = corners[i].clone();

	// Project the vector to screen coordinates
	const widthHalf = 0.5 * (this.camera.right - this.camera.left);
	const heightHalf = 0.5 * (this.camera.top - this.camera.bottom);

	vector.project(this.camera);

	const screenX = (vector.x * widthHalf) + widthHalf;
	const screenY = -(vector.y * heightHalf) + heightHalf;

	// Store the screen coordinates
	screenCoords.push({ x: screenX, y: screenY });
}

// Calculate the projected bounding rectangle
let minX = Infinity, minY = Infinity;
let maxX = -Infinity, maxY = -Infinity;

for (let i = 0; i < screenCoords.length; i++) {
	const coord = screenCoords[i];
	if (coord.x < minX) minX = coord.x;
	if (coord.y < minY) minY = coord.y;
	if (coord.x > maxX) maxX = coord.x;
	if (coord.y > maxY) maxY = coord.y;
}

// Calculate the dimensions of the projected bounding rectangle
const boundingWidth = maxX - minX;
const boundingHeight = maxY - minY;

// Get the dimensions of the camera's frustum
const frustumWidth = this.camera.right - this.camera.left;
const frustumHeight = this.camera.top - this.camera.bottom;

// Calculate the zoom factor needed to fit the bounding rectangle in the viewport
const zoomWidth = (frustumWidth - (frustumWidth * CAMERA_FIT_WIDTH_MARGIN)) / boundingWidth;
const zoomHeight = (frustumHeight - (frustumWidth * CAMERA_FIT_HEIGHT_MARGIN)) / boundingHeight;

// Set the camera zoom to the smaller of the two zoom factors, but invert the calculation since higher zoom means smaller view
const newZoom = this.cosmos.graphCamera.zoom * Math.min(zoomWidth, zoomHeight);

// And finally animate to the new zoom and position
this.camControls.saveState();

this.camControls.zoomTo(newZoom);
this.camControls.moveTo(center.x, center.y, center.z);

Here is a video of it running, with vertical and horizontal margins set at 10% of the viewport and with a max zoom of 1.
Notice how it works perfectly even when rotating/moving the camera at the end of the video :slight_smile:

5 Likes

looking good!