~3 min read

Path3D, PathFollow3D and splines

Curve3D, Path3D, PathFollow3D — movement along a spline, AI patrols, camera cutscenes.

Godot has built-in Path3D and PathFollow3D for movement along a spline. The curve is stored in a Curve3D resource. No plugins required — it’s part of the core engine.

Main nodes

  • Path3D — a container node with a Curve3D resource. Describes a curve in space.
  • PathFollow3D — a child node of Path3D. Holds a progress or progress_ratio property from which Godot computes the position along the curve. Anything placed inside PathFollow3D travels along the spline.
  • Curve3D — a resource with an array of points, each having a position + in/out tangents (for Bezier).
Web

An SVG path with M, L, C commands. Or the GSAP MotionPath plugin for animation along a curve.

Unity

Basic workflow

Creating a Path3D in the editor

  1. Add a Path3D to the scene.
  2. In the Inspector, create a new Curve3D (if there isn’t one yet).
  3. In the Scene view, select the Path3D — the Path Tool appears in the toolbar.
  4. Click to add points. Drag the handles to edit the tangents (Bezier).

Scene structure

Level (Node3D)
└── Path3D                  ← curve: Curve3D with N points
    └── PathFollow3D
        └── Cart (CharacterBody3D or Node3D)  ← will travel along the spline

Movement from code

extends PathFollow3D

@export var speed: float = 4.0
@export var loop_mode: int = 0  # 0 = loop, 1 = stop at end

func _process(delta: float) -> void:
    progress += speed * delta
    # progress — distance in meters along the curve
    # progress_ratio — normalized 0..1 (convenient for looping)

PathFollow3D properties:

  • progress: float — the current position along the curve in meters.
  • progress_ratio: float — the normalized parameter [0, 1].
  • loop: bool — whether to loop (default true).
  • rotation_mode: RotationModeROTATION_NONE / _Y / _XY / _XYZ / _ORIENTED. ORIENTED rotates along the curve’s tangent (for a car driving along a road).
  • use_model_front: bool — corrects orientation (if the model faces -Z).
progress vs progress_ratio

progress is the distance in meters along the spline. With speed in m/s, progress += speed * delta gives a constant speed. progress_ratio (0..1) is more convenient for UI or duration-based animation (“travel the whole curve in 5 seconds”), but then the speed depends on the curve’s length.

Creating a curve programmatically

var curve := Curve3D.new()
curve.add_point(Vector3(0, 0, 0))
curve.add_point(Vector3(5, 0, 0))
curve.add_point(Vector3(5, 2, -5),
    Vector3(0, 0, 2),    # in-tangent — entering the point from the previous one
    Vector3(0, 0, -2))   # out-tangent — leaving toward the next one
curve.add_point(Vector3(0, 0, -10))

var path := Path3D.new()
path.curve = curve
add_child(path)

add_point(position, in_handle, out_handle, index) — without in/out it produces straight segments, with them — Bezier curves.

Sampling without a PathFollow3D node

Curve3D has methods for direct sampling:

var point := curve.sample(0.5)            # point at the midpoint (normalized)
var point_baked := curve.sample_baked(distance_m)  # point at distance_m meters along
var transform := curve.sample_baked_with_rotation(distance_m, true, true)

sample_baked uses a baked length cache — an internal array of precomputed points that is updated when the curve changes. Sampling by distance runs in O(log N).

Camera on rails (cinemachine-style)

Godot has no built-in equivalent of Cinemachine SplineDolly, but doing it yourself takes 5 lines:

extends Camera3D

@export var path: Path3D
@export var duration: float = 5.0
@export var look_at: Node3D

var _t: float = 0.0
var _playing: bool = false

func play() -> void:
    _t = 0.0
    _playing = true

func _process(delta: float) -> void:
    if not _playing or path == null:
        return
    _t += delta / duration
    if _t >= 1.0:
        _t = 1.0
        _playing = false

    var distance := path.curve.get_baked_length() * _t
    var pos := path.curve.sample_baked(distance)
    global_position = path.to_global(pos)

    if look_at != null:
        look_at(look_at.global_position, Vector3.UP)

Or use the Phantom Camera plugin (mentioned in the camera chapter) — it has a Path follow mode.

Placing objects along a spline

Godot core has no built-in “Instantiate along path” — you have to do it yourself. A simple tool script via @tool:

@tool
extends Node3D

@export var path: Path3D
@export var prefab: PackedScene
@export var spacing: float = 2.0
@export var update: bool = false:
    set(value):
        if value: _rebuild()
        update = false

func _rebuild() -> void:
    for child in get_children():
        child.queue_free()
    if path == null or prefab == null:
        return
    var length := path.curve.get_baked_length()
    var count := int(length / spacing)
    for i in count:
        var instance := prefab.instantiate()
        var pos := path.curve.sample_baked(i * spacing)
        instance.position = pos
        add_child(instance)
        instance.owner = get_tree().edited_scene_root

Make this node a child of Path3D, drag in the prefab, press update in the Inspector — it places N instances with a step of spacing.

When to use splines

  • AI patrol along a predefined route — PathFollow3D + ROTATION_Y.
  • Cars on a track — PathFollow3D + ROTATION_ORIENTED.
  • Camera cutscenes — Camera3D on a PathFollow3D + a look_at target.
  • Conveyor belts, cargo carts — anything that moves along a fixed trajectory.
  • Trail effects — a position behind the player, sampled from the recent trail.

When NOT to use them

  • Straight segments — an array of Vector3 is cheaper.
  • NavMesh navigation over uneven terrain — Path3D has no obstacle avoidance. Use NavigationAgent3D (see the Navigation chapter).
  • Hundreds of dynamically created curves per frame — for heavy cases use WorkerThreadPool (see the next chapter) or GDExtension.