How to Center the View on a Point

Godot Version

4.5

Question

I recently switched from Unity to Godot and I’m still adjusting. So far, the experience has been great, but I was wondering about one small yet very useful feature that many 3D applications have: the ability to center the editor camera around a point.

In Unity, I can simply middle-click anywhere in the scene, and the editor camera automatically centers on that spot. This lets me easily rotate around it, and any zoom or pan movements are adjusted relative to that point. Blender offers the same feature, and once you get used to it, it becomes indispensable.

So, I’m curious—does Godot have a similar function?

1 Like

You need to select the necessary node and press F.

2 Likes

Not quite. I’m looking to center the camera around the clicked point, not around a node. To make it clearer, I’ll just show you.

In this Godot video, I’m trying to center the camera around the top of the tower with the small “antenna” so I can orbit around that point and inspect it. It is possible, but in my opinion the process is very cumbersome. (Sorry about external link, forum won’t let me upload files because I’m a new user)

Godot

I also recorded another video, this time in Unity, doing the same thing. In contrast, I can simply middle‑click on the top of the tower and the view is instantly centered around the clicked point. For demonstration, I clicked a few different spots to show how easy it is to center the camera around any point.

Unity

So my question is: does Godot have a similar feature? Since this was added to Blender and Unity, it has become essential for me because it greatly simplifies navigating the 3D inspector.

Afaik, there’s no such functionality as of 4.5. Maybe it’s planned for some upcoming version.

Making a plugin shouldn’t be too hard though.

1 Like

Here’s a draft plugin. You can try it out and see how it works for you.

The command is ALT + middle click. It works only on meshes and ground. Could be extended to colliders.

It ended up a little bit trickier than I expected. Mesh picking works via gizmos but gizmo functionality that allows on-demand ray hit testing is not exposed to scripting i.e. EditorNode3DGizmo::intersect_ray()
Because of this, the plugin needs to pluck triangular meshes from the gpu buffers and do the “manual” ray testing. This is currently done brute force every time the user alt clicks, for all mesh instances. Performance needs to be tested with mesh heavy scenes. Plenty of room for improvement here.

I also used a rather hacky method to execute the built in focusing. Again the control over camera cursor is not exposed to scripting so the only way to do this is tricking the editor to execute the menu callback. If someone knows a better way - please share.

Doing this as a native code extension or source code intervention would be quite a bit simpler as there is access to all of the needed functionality via just a few calls, but then the thing would need to be compiled per platform. So this may be a good enough compromise.

@tool
extends EditorPlugin


func _enable_plugin() -> void:
	# enable forwarding of 3d viewport events even if no node is selected
	set_input_event_forwarding_always_enabled()


func _forward_3d_gui_input(viewport_camera, event) -> int:
	# do our thing on ALT + MMB
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_MIDDLE and event.is_pressed() and event.alt_pressed:
			focus_cam_to_surface_point(event.position, viewport_camera)
	return AFTER_GUI_INPUT_PASS
	 
	
func focus_cam_to_surface_point(screen_position: Vector2, cam: Camera3D) -> void:
	# get all mesh instances in the scene
	var scene_root: Node = EditorInterface.get_edited_scene_root()
	var mesh_instances: Array = scene_root.find_children("*", "MeshInstance3D", true, false)
	
	# get pick ray in world space
	var ray_origin: Vector3 = cam.project_ray_origin(screen_position)
	var ray_direction: Vector3 = cam.project_ray_normal(screen_position)
	
	# collect pick ray hitpoints on all meshes. SLOW! could use some optimization (e.g. cache the triangle meshes)
	var hitpoints: Array[Vector3] = []
	for instance: MeshInstance3D in mesh_instances:
		# make trimesh
		var trimesh: TriangleMesh = instance.mesh.generate_triangle_mesh()
		# global to instance local matrix
		var to_instance_space = instance.global_transform.affine_inverse()
		# transform the pick ray into instance local space
		var ray_origin_local: Vector3 = to_instance_space * ray_origin
		var ray_direction_local: Vector3 = to_instance_space.basis * ray_direction
		# intersect and append hitpoint if mesh was hit
		var result: Dictionary = trimesh.intersect_ray(ray_origin_local, ray_direction_local)
		if result:
			hitpoints.append(instance.global_transform * result["position"])
	
	# sort hitpoints by distance to camera
	hitpoints.sort_custom(
			func(a: Vector3, b: Vector3):
				return cam.global_position.distance_squared_to(a) < cam.global_position.distance_squared_to(b)
	)
	
	# decide the target position: nearest hitpoint or ground if there are no hitpoints
	var target_position: Vector3
	if hitpoints.is_empty():
		var plane_hit: Variant = Plane(Vector3.UP).intersects_ray(ray_origin, ray_direction) 
		if plane_hit:
			target_position = plane_hit
		else:
			return # ray cannot hit ground, we're out of target options
	else:
		target_position = hitpoints[0]
	
	# create a dummy target node
	var dummy_target: Node3D = Node3D.new()
	scene_root.add_child(dummy_target)
	dummy_target.owner = scene_root # set the owner so we can see the node in the editor if something fails, just for debugging
	dummy_target.global_position = target_position
	
	# store the current selection and select the dummy target node
	var editor_selection: EditorSelection = EditorInterface.get_selection()
	var selected_nodes_before: Array = editor_selection.get_selected_nodes()
	editor_selection.clear()
	editor_selection.add_node(dummy_target)
	
	# trigger editor's built in focus by faking the menu hotkey hit, happens in all 4 views
	# (find a better way to do this)
	for b in EditorInterface.get_editor_main_screen().find_children("*", "Button", true, false):
		if b.text.contains("Perspective"):
			var p: PopupMenu = b.find_children("*", "PopupMenu", false, false)[0]
			var e: InputEventKey = InputEventKey.new()
			e.keycode = KEY_F
			e.pressed = true
			p.activate_item_by_event(e)

	# delete the dummy target node and restore the selection
	dummy_target.queue_free()
	for node in selected_nodes_before:
		editor_selection.add_node(node)
6 Likes

Wow, that just works, thank you so much for taking the time to create a solution.

I hope they add this functionality to the engine in the future, it’s such a nice addition.

1 Like

This would definitely be neat to have built in. Maybe some of the higher-ups know if it’s already on the short term to do list, @athousandships?
If not, I might tidy it up a bit and upload to the asset lib.

4 Likes

The plugin is now in the asset lib:

@vhuuuu consider using the asset lib version as it’s more ironed out compared to the above code. Things that were missing but are added now:

  • when in multiple view mode, the click only affects the current viewport, not all of them
  • invisible objects are ignored
  • picking meshes are created per unique mesh resource, not per instance (an obvious low hanging fruit performance optimization)

Comments and suggestions are welcome.

If there is enough interest, I might consider implementing this natively and trying for a pull request.

3 Likes

Great work man, I thank you again, this should definitely come with the engine by default.

1 Like

I actually made a source code implementation and a proposal but turns out there already was a still open pull request from 6 months ago. The main point of contention is the control scheme. The simple ALT+MMB is apparently a no go because of recent addition of customizable camera navigation schemes that emulate other popular 3D programs (Maya, Modo etc). My suggestion was to use all 3 mod keys + mouse click so it doesn’t interfere with any of those classic nav schemes. Either that or a single hotkey.

All in all, the thing works superbly when integrated natively. Hopefully we’ll see it in the next release. If not, you can always compile my fork :slight_smile:

2 Likes

I wish they just added it to the engine, too bad it conflicts with nav schemes.
Is there a way to make a request to the dev team to include this feature?

The pull request is already there. You can go upvote and comment on it. You can upvote my proposal as well, although it was closed. Doing so may increase the likelihood of lead developers paying more attention to the whole thing.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.