Trouble zooming camera to cursor

Godot Version

4.5.1

Question

When trying to create a controller for Camera2D I needed to make it so the camera is going to zoom to the current cursor position.

The expected behavior

When I scroll the wheel the camera will zoom in/out and the point just under the cursor will not offset relative to cursor.

The problem

After the first tries, learning and searching the answers on the forum and reddit I thought that I needed to capture the cursor position before and after zooming. Then I need to offset the camera position by the difference between the old and new cursor positions.

This is the script that I came up with:

extends Camera2D


var zoom_step: float = 0.05
var zoom_min: float = 0.05
var zoom_max: float = 10.0


func _input(event: InputEvent) -> void:
	if event.is_action(&"zoom_in"):
		zoom_camera(1.0 + zoom_step)
	if event.is_action(&"zoom_out"):
		zoom_camera(1.0 - zoom_step)


func zoom_camera(zoom_factor: float) -> void:
	var new_zoom: Vector2 = (zoom * zoom_factor)
	new_zoom = new_zoom.clamp(
		Vector2(zoom_min, zoom_min),
		Vector2(zoom_max, zoom_max)
	)
	var mouse_position: Vector2 = get_global_mouse_position()
	zoom = new_zoom
	var new_mouse_position: Vector2 = get_global_mouse_position()
	global_position += mouse_position - new_mouse_position

However the desired behavior isn't satisfied by this approach because after offsetting the camera by the difference of 2 cursor positions the point under the cursor is being offset after zooming.

:(

For the context, for the testing I created a scene with only Node2D as a root, a Camera2D and a couple of Sprite2D nodes for zooming reference.

I tried to debug the scene by placing 2 Polygons2D and changing the global_position of the first to correspond mouse_position value and the second to correspond the new_mouse_position value. This didn't get me too far, I just got a confirmation that something is wrong as I saw how their positions differ.

I’m not sure how to debug this and my only guess is that Camera2D position and/or offset is going through some changes made by viewport or something else.

I don’t really know how to proceed next :face_with_spiral_eyes:

This was the best I could do with the feature you wanted, it seems pretty jarring I think

extends Camera2D

var zoom_step: float = 0.05
var zoom_min: float = 0.05
var zoom_max: float = 10.0

const SMOOTH_SPEED: float = 10.0
var target_zoom: Vector2
var target_position: Vector2

func _ready() -> void:
	target_zoom = self.zoom
	target_position = self.global_position

func _input(event: InputEvent) -> void:
	if event.is_action_pressed(&"zoom_in"):
		set_new_targets(1.0 + zoom_step)

	if event.is_action_pressed(&"zoom_out"):
		set_new_targets(1.0 - zoom_step)

func _process(delta: float) -> void:
	self.global_position = self.global_position.lerp(target_position, delta * SMOOTH_SPEED)
	self.zoom = self.zoom.lerp(target_zoom, delta * SMOOTH_SPEED)

func set_new_targets(scale_factor: float) -> void:
	var potential_zoom: Vector2 = self.zoom * scale_factor
	var clamped_zoom = potential_zoom.clamp(
	Vector2(zoom_min, zoom_min),
	Vector2(zoom_max, zoom_max)
	)
	if clamped_zoom == self.zoom:
		return
	target_zoom = clamped_zoom
	var mouse_world_pos: Vector2 = get_global_mouse_position()
	target_position = mouse_world_pos

Thank you for your efforts. Your solution doesn’t satisfy the problem, however.

That’s because when I zoom in/out the point under the cursor is being offset and then the camera position is being adjusted further in the _process function. This means when I zoom in/out 2 times in a short amount of time the camera will be corrected to the last target_position only.

Do you have an example of the wanted behavior? A video from some existing game or a mockup animation.

Well, that’s the behavior of the godot’s 2d editor camera. The default behavior of Camera2D zooming is that it is zooming to the center of the view. The camera inside the editor is zooming to the current position of the cursor.

I’ll try to record a short example tomorrow if needed.

Not sure this is doable via camera node. You’ll likely need to go directly to viewport’s canvas_transform.

1 Like

Yes, I tried to do so today as it was suggested by the documentation of the Camera2D. However I faced a similar problem essentially :frowning:

For translating I affected viewport.canvas_transform.origin and for scaling I used viewport.canvas_transform.scaled() method.

I tried a different approach of first moving the viewport origin to cursor, scaling and then moving back to original position. Tried also multiplying/dividing the second offset by zoom, but everything I tried was not successful.

I’m basically back at step 1 at a time of writing my post here

Try this:

extends Node2D

const scale_step := Vector2(1.1, 1.1)
@onready var vp := get_viewport()

func _input(e):
	# pan
	if e is InputEventMouseMotion:
		if e.button_mask & MOUSE_BUTTON_MASK_MIDDLE:
			vp.canvas_transform = vp.canvas_transform.translated(e.relative)
	# zoom
	if e is InputEventMouseButton and e.is_pressed():
		if e.button_index != MOUSE_BUTTON_WHEEL_DOWN and e.button_index != MOUSE_BUTTON_WHEEL_UP:
			return
		vp.canvas_transform = vp.canvas_transform.translated(-vp.get_mouse_position())
		if e.button_index == MOUSE_BUTTON_WHEEL_DOWN:
			vp.canvas_transform = vp.canvas_transform.scaled(Vector2.ONE / scale_step)
		if e.button_index == MOUSE_BUTTON_WHEEL_UP:
			vp.canvas_transform = vp.canvas_transform.scaled(scale_step)
		vp.canvas_transform = vp.canvas_transform.translated(vp.get_mouse_position())
1 Like