Hi, thanks for reaching out! I didn’t know about your project at Smplrspace yet, but it sounds really interesting. It’s always great to find someone else working with Babylon.js for interior projects—there aren’t that many of us out there!
About your question:
My first hack with zoom was inspired by experiments like these:
If you don’t have strict requirements for pan or orbit, these examples can be pretty useful (though they definitely need some customization for production use). My main struggle with that approach was finding a solid way to apply vertical panning in the scene—never quite got it working the way I wanted.
That’s what pushed me to try a new direction using zoomToMouseLocation
. It gave me a cleaner result and made it possible to apply adaptive sensitivity for PAN, ZOOM, and ORBIT, which was a huge win for user experience. The catch is that I had to adopt a behavior where the target point is reset by user-directed clicks—both to achieve precise zooming (especially for elements positioned behind the stage center) and to enable orbiting around the right point.
The project I’m rebuilding isn’t public just yet, but I expect to open it up in a few weeks or maybe a couple of months. When it’s ready, I’d love to share a demo or give you early access. Meanwhile, if you want to talk about Babylon hacks or trade experiences, just let me know!
By the way, I made a few UX tweaks around zoomToMouseLocation
and dynamic target changes that really improved the navigation feeling in my Babylon app. Maybe you’ll find something useful in this approach:
- Smooth animation for transitions: Whenever I set a new target (after a left-click on a mesh), I animate both the camera’s target and radius for a more natural movement.
- Mouse button mapping: I remapped the camera controls so left-click only changes the target (and triggers the smooth animation), which avoids accidental orbits/pans during navigation.
- Adaptive camera sensitivity: I interpolate the pan, zoom, and orbit sensitivity dynamically based on the camera’s distance to the target. That way, you get very precise control when you’re close, and fast navigation when you’re far away.
Here’s a stripped-down version of how I’m handling these aspects:
// Initial camera config (example values)
var homeAlpha = 4.15;
var homeBeta = 1.15;
var homeRadius = Math.max(stageLenY * 1.5, stageLenX, stageLenZ) / 10;
var homeTarget = new BABYLON.Vector3(
-stageLenX / 10 / 4 * 0.75,
stageLenY / 10 / 2,
-Math.max(stageLenY / stageLenX, stageLenZ) / 10 / 4 - 50
);
camera = new BABYLON.ArcRotateCamera(
'camera',
homeAlpha,
homeBeta,
homeRadius,
homeTarget,
scene
);
camera.upperRadiusLimit = 5000;
camera.lowerRadiusLimit = 0.01;
camera.inertia = 0.5;
camera.zoomInertia = 0.1;
camera.panningInertia = 0.5;
camera.panningAxis = new BABYLON.Vector3(1, 1, 0);
camera.angularSensibilityX = 1500;
camera.angularSensibilityY = 1500;
camera.zoomToMouseLocation = true;
camera.attachControl(canvas, true);
// Disable default left mouse button camera event
camera.inputs.attached.pointers.buttons = [2, 1, 2];
// Set new orbit target on left click, with animation
scene.onPointerObservable.add(function(pointerInfo) {
if (pointerInfo.type === BABYLON.PointerEventTypes.POINTERPICK) {
if (pointerInfo.event && pointerInfo.event.button === 0) {
var pick = scene.pick(scene.pointerX, scene.pointerY);
if (pick.hit && pick.pickedPoint) {
var bb = pick.pickedMesh.getBoundingInfo().boundingBox;
if (!bb || !bb.intersectsPoint(pick.pickedPoint)) return;
var dist = BABYLON.Vector3.Distance(camera.target, pick.pickedPoint);
if (dist < 5) return; // Ignore if too close
var newRadius = BABYLON.Vector3.Distance(camera.position, pick.pickedPoint);
animateCameraTargetAndRadius(camera, pick.pickedPoint, newRadius, 150);
}
}
}
});
// Adaptive camera sensitivity based on radius/target distance
scene.onBeforeRenderObservable.add(function() {
var minPanSens = 200, maxPanSens = 5;
var minZoom = 0.05, maxZoom = 0.1;
var minOrbit = 3000, maxOrbit = 500;
var minRadius = 0.005, maxRadius = 500;
var t = Math.log(camera.radius - minRadius + 1) / Math.log(maxRadius - minRadius + 10);
t = Math.max(0, Math.min(1, t));
camera.panningSensibility = minPanSens - (minPanSens - maxPanSens) * t;
camera.wheelDeltaPercentage = minZoom + (maxZoom - minZoom) * t;
camera.angularSensibilityX = minOrbit - (minOrbit - maxOrbit) * t;
camera.angularSensibilityY = minOrbit - (minOrbit - maxOrbit) * t;
});
function animateCameraTargetAndRadius(camera, newTarget, newRadius, durationMs) {
var fps = 60;
var totalFrames = (durationMs / 1000) * fps;
var animX = new BABYLON.Animation("animTargetX", "target.x", fps,
BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
animX.setKeys([{ frame: 0, value: camera.target.x }, { frame: totalFrames, value: newTarget.x }]);
var animY = new BABYLON.Animation("animTargetY", "target.y", fps,
BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
animY.setKeys([{ frame: 0, value: camera.target.y }, { frame: totalFrames, value: newTarget.y }]);
var animZ = new BABYLON.Animation("animTargetZ", "target.z", fps,
BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
animZ.setKeys([{ frame: 0, value: camera.target.z }, { frame: totalFrames, value: newTarget.z }]);
var animRadius = new BABYLON.Animation("animRadius", "radius", fps,
BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
animRadius.setKeys([{ frame: 0, value: camera.radius }, { frame: totalFrames, value: newRadius }]);
camera.animations = [animX, animY, animZ, animRadius];
camera.getScene().beginAnimation(camera, 0, totalFrames, false);
}
Happy to share more or chat further if you’re interested! Thanks again for the message 
Cheers,
Junior