How to Center the View on a Point

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