How would I get the closest point on a Curve3D in world space?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By James Kelly

get_closest_point works in the curve’s local space but I need to be working in world space, so somehow I need to be able to transform a point between the two.

I don’t think a PathFollow will work for what I have in mind since it takes a scalar offset (Between 0 and 1) and what I’m trying to accomplish is to have a camera clamped to a figurative rail (A Curve3D from a Path node in this case) and I need to find where on this rail is closest to an arbitrary position such as a player.

:bust_in_silhouette: Reply From: idletalent

I found this question yesterday when I was searching for a solution to the same problem, and decided to leave this tab open in case I figured it out.

I’m no Godot expert, so this might not be the best way to do this, but it’s working for my use case, and might help you get where you want to be.

First I want to bring something about PathFollow to your attention. You say the problem is that it takes a scalar for its offset, which it does as unit_offset, but there is also a property for its actual offset (just called offset) which is as long as the path.

I’m also using get_closest_offset() instead of get_closest_point() because it will return something I think is easier to work with.

To solve the problem of converting the Player’s World Space to the Path’s Local Space to be able to use get_closest_offset(), I made a Position3D as a child of the Path, called it follow_target, then in _physics_process() matched it’s global_transform.origin to the player:

follow_target.global_transform.origin = player.global_transform.origin

Then I get the translation (i.e. local space) of the follow_target and use that to get_closest_offset() of the path.curve, and set it as the PathFollow’s offset.

path_follow.set_offset(path.curve.get_closest_offset(follow_target.get_translation()))

This works fine for straight paths, and probably works for linear paths that don’t double back on themselves, but if you have a horseshoe-shaped camera path (which I did), it can make the PathFollow rush from end to end (i.e. the player can be closest to the beginning of the path, then closest to the end of the path without passing through any of the intermediate points).

To solve this, I created a second path, which I called reference_path, which is a straight line from the beginning to the end of the path I want to lock the camera to.
Then I use the above code to set the offset on the reference_path, then copy the unit_offset of the PathFollow on the reference_path to the unit_offset of the PathFollow on the camera_path.
N.B. using unit_offset for this part because the paths are different lengths, so their regular offsets won’t match up.

So that whole script keeps a PathFollow in the position I want the camera, and looks like this:

extends Spatial


onready var reference_path: = $ReferencePath
onready var reference_path_follow: = $ReferencePath/PathFollow
onready var path_follow: = $Path/PathFollow
onready var follow_target: = $Path/FollowTarget
onready var player = get_tree().get_nodes_in_group("player")[0]


func _physics_process(_delta: float) -> void:
	follow_target.global_transform.origin = player.global_transform.origin
	reference_path_follow.set_offset(reference_path.curve.get_closest_offset(follow_target.get_translation()))
	path_follow.set_unit_offset(lerp(path_follow.unit_offset, reference_path_follow.get_unit_offset(), 1))

Then I pass the path_follow to my CameraRig as rails_cam_position, and in the _physics_process(), I lerp from the camera_rig.global_transform.origin to the rails_cam_position.global_transform.origin.

func physics_process(delta: float) -> void:
	camera_rig.global_transform.origin = Vector3(lerp(camera_rig.global_transform.origin, camera_rig.rails_cam_position.global_transform.origin, 10 * delta)

My camera uses look_at() to always face the player, so none of this code takes the path’s rotation into consideration, so if you get weird rotation issues, you’ll want to tinker with your PathFollow’s Rotation Mode settings, I guess.

I hope this is helpful :slight_smile:

This worked really well for me. Thanks!

jmtstorres | 2020-08-28 13:46

:bust_in_silhouette: Reply From: DaddyMonster

This is very old but I just googled this myself as I didn’t know Curve3D had get_closest_offset(). The original answer to manage local space / world space is very ingenious but this can be done in one line just by multiplying in global vector by the curve’s inverse transform matrix (because maths):

func get_offset():
    return path.curve.get_closest_offset(path.transform.xform_inv(global_transform.origin))

Hopefully this will make life easier for someone googling this in the future.