How to implement true "zoom under cursor" (CAD-style) with ArcRotateCamera in BabylonJS 8.x?

Hi everyone,

I’m working on a 3D application and trying to deliver a CAD-style “zoom under cursor” experience for my users—meaning the camera should zoom toward the point under the mouse cursor, not just the target or center of the scene.

In my research, even ChatGPT and other sources mention a property called useInputToZoomOnPointerLocation for the ArcRotateCamera, but I could not find this property in the current BabylonJS source code, documentation, or any official examples in version 8.x.
It seems that if this property ever existed, it has been removed and is no longer available.

Current behavior:
ArcRotateCamera always zooms centered on its target, regardless of where the mouse pointer is, which is different from the behavior expected by users coming from CAD, SketchUp, Promob, etc.

Expected (CAD-style) behavior:
When scrolling the mouse wheel, the camera should zoom in toward the point under the cursor, so users can focus on details and navigate large scenes more intuitively.


My question

Is there an official or recommended way to achieve zoom under cursor with ArcRotateCamera in BabylonJS 8.x?
If not, does anyone have a reliable workaround, approach, or code snippet for a custom implementation that behaves well in complex scenes?

Any insights or suggestions are greatly appreciated!

Thank you in advance!

1 Like

Hi @juniorconte and welcome to the community!

There’s ArcRotateCamera.zoomToMouseLocation - is this what you’re after?

This was discussed (and implemented) here:

Note it works for mouse wheel zoom only and not touch device pinch zoom.

Possibly your ChatGPT response was an AI hallucination :slight_smile:

The Babylon.js team are committed to backwards compatibility, so it’s very rare and they require strong justification to remove or change any public methods in the API.

4 Likes

hi @inteja

Thank you so much for your quick and clear answer!

Setting ArcRotateCamera.zoomToMouseLocation = true worked perfectly — it delivers exactly the CAD-like “zoom under cursor” experience I needed.
My users can now zoom in on any point in the scene much more intuitively.

This single flag made my code much simpler and the navigation much more professional.

Alongside zoomToMouseLocation, I also implemented contextual setTarget (to let users orbit around specific elements) and dynamic adjustments to pan, zoom, and orbit precision based on camera distance.
Combining these approaches worked really well.
This new setup allowed me to finally remove an old custom zoom hack that caused a lot of headaches, especially with vertical pan.

Thanks again for the help, and congrats to the BabylonJS team for such a powerful API!

:smiling_face_with_three_hearts:

3 Likes

FYI the ‘Other sources’ are generally defunct forum posts where gpt scraped its answer from, or worse and more common, llm regurgitations of such posts. And even when not conveying old and/or bad information, GPT often just invents methods that don’t exist to tell you what you want to hear. This happens in every language/domain I’ve seen so far.
Just, User beware, you have to help it work through the api.

Thanks for your observation — it’s a very valid and important point!
I completely agree that nothing replaces real testing, documentation, and advice from those who know the library inside out.
Just to add a bit to the discussion: I’ve actually been using GPT-4.1 a lot as a “pair programmer” when working on complex refactoring tasks, especially for this project I’ve been evolving since 2020. I’m modernizing several parts of the codebase to take full advantage of native Babylon 8 features.

In my experience, GPT can help brainstorm approaches, spot possible issues, and speed up “thinking out loud,” but I always verify everything in the docs, the API, and with the community — as you wisely recommend.
Thanks again for the heads up and for keeping the forum a reliable place for solid info and good practice!

1 Like

Hi @juniorconte, the app you’re developing looks pretty awesome. I’m working on indoor space visualization as well at Smplrspace :slight_smile: .

I’m using zoomToMouseLocation in our 2D viewer, but in 3D I found it could lead to confusing end camera positions, that made it hard for users to recover to a typical top view. Did you face a similar issue?

Also this sentence is tickling my curiosity:

It sounds really interesting. Is it possible to test this anywhere? I’d love to understand the experience you manage to deliver.

:victory_hand:

1 Like

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 :slight_smile:

Cheers,
Junior

2 Likes

@tibotiber if I understand your issue correctly, we faced something similar.

We implemented an ArcRotateCamera “infinite zoom” which detects when a zoom in event has completed, then modifies the target, pushing it further away (along camera view vector), and on zoom out after a certain threshold the target can optionally be dragged closer. This allows the user to rotate around a more natural/intuitive centre and (with pan) explore a much larger scene than ArcRotateCamera typically allows with its zooming to a fixed target.

It does get tricky though as camera zoom/pan precision values, changing radius and target are interconnected. It’s also hard to balance from a UX standpoint too, because if the values are off slightly, it can result in user WTF moments as they have no idea the camera target is changing (unless you choose to display a crosshair or something).

2 Likes

Thanks for sharing that much about your approach @juniorconte!

We have so much shared experience, we basically hand rolled a panning system on top of the arc rotate behavior (using ctrl+drag) but faced a similar issue with vertical panning, and also haven’t got it yet exactly where we want it to be.

We’ve also got pretty much the same animateCameraTargetAndRadius but trigger it on double click. The adaptive camera sensitivity is really neat, I might get heavily inspired :wink: .

I’d be really happy to chat some day about all this. Unfortunately, I’m swamped at the moment with feature releases and upcoming trips. Can I check in with you later in the year when things calm down?

And good to hear from @inteja, it’s been a while. I’m actually wondering if it wasn’t you who put me on the path to zoomToMouseLocation initially, somewhere in this forum :flexed_biceps: .

Honestly I can’t recall the specifics of the issue, that was at least 2 years back. But I think it came from the fact that if you’re not a CAD person, zoom in is great but zoom out in 3D is controlled by where your cursor is, which at this point feels unnatural. You kinda zoom in to the cursor, then move the cursor to look at what you zoomed in on, at which point the cursor location is irrelevant, and then you zoom out thinking you’ll end up where you started but you don’t. So potentially you start with a floor centered, zoom in to a side (yayy), then zoom out and the whole floor is off center.

Most of our users are less digitally experienced, so this becomes super confusing to them. Heck, it honestly frustrates me that it doesn’t “just work TM”.

1 Like