---
title: "Paths: splines, roads, tracks, rails, lanes and routes"
description: "Three pieces cover anything that runs along a line through the world:"
section: "Topics"
slug: "topics-paths"
canonical: "https://origozero.ai/docs/topics-paths"
updated: "2026-10-26T11:50:21.647260205+00:00"
---

# Paths: splines, roads, tracks, rails, lanes and routes

| Piece | What it is |
|---|---|
| `@builtin::modules.spline` | Curve math: linear, Catmull-Rom and Bezier splines, eased sampling, placement along a curve (fence posts, rows of props), and measuring a curve by arc length. |
| `@builtin::modules.pathNetwork` | Networks of paths built at runtime: roads, streets, race tracks, railway and tram lines, footpaths, river courses, patrol and delivery routes. Drivable surfaces with colliders and a material, junctions, terrain cut and fill, directed lanes, and route queries. |
| `PathFollower` component | Moves an entity along a path at a constant speed: a train, a tram, a patrol, a camera dolly, a ghost car. |

## A single curve

```luau
local spline = require("@builtin::modules.spline")
local path = spline.new({ { 0, 0, 0 }, { 20, 2, 10 }, { 40, 0, 0 } }, { mode = "catmullRom" })
local mid = spline.evaluate(path, 0.5)          -- by parameter
local m = spline.measure(path)                   -- by arc length
local p, dir = spline.atDistance(m, 12)          -- 12 metres along, constant speed
local posts = spline.fence(path, 2.5)            -- placements every 2.5 m
```

A spline's parameter does not advance at a constant rate along the curve, so
anything that moves in metres per second reads the measured form.

## Moving something along a path

```luau
local e = entity.spawn("tram")
e.component.add("Model", { model = "@builtin::meshes.cube" })
e.component.add("PathFollower", { speed = 8, mode = "loop" })
e.component.get("PathFollower"):follow(spline.new(points, { mode = "catmullRom", closed = true }))
```

`PathFollower` takes a list of points, a spline, or anything carrying `points`
(a network route, a lane loop), and loops, ping-pongs or stops at the end. It
sets the entity's transform, so it drives entities with no dynamic body. A
physics vehicle follows a path by steering instead (below).

## A network of paths

```luau
local paths = require("@builtin::modules.pathNetwork")
local net = paths.new({ lanes = { forward = 1, backward = 1 }, speedLimit = 15 })
local a = net:addNode({ 0, 0, 0 })
local b = net:addNode({ 200, 0, 40 })
local c = net:addNode({ 380, 0, -60 })
net:addEdge(a, b, { points = { { 100, 0, 40 } } })
net:addEdge(b, c, { bank = 5 })
net:build()
```

- **Edges** run through control points; settings per edge or per network:
  lanes each way, lane width, shoulder, speed limit, bank, material, tint and
  the collider's fields (`friction`, ...).
- **Junctions** form where three or more edges meet; the edges stop short and
  a junction surface joins their ends.
- **Elevation**: `"terrain"` lays the surface on the ground (smoothed);
  `"points"` takes the control points' heights. A bridge is an edge with
  `elevation = "points"` and `conform = false`.
- **Terrain**: with a `Terrain` in the scene, `build` cuts and fills the ground
  to meet each surface and blends it back over `falloff` metres, through
  `Terrain:setHeights`, which rebuilds the drawn ground and its collision where
  the change reaches. Moving or removing a path gives the ground back.
- **Live editing**: `moveNode`, `setEdge`, `removeEdge`, then `build()` again
  rebuilds only what the edit reaches; `build` answers what it rebuilt and the
  milliseconds it took.
- **Procedural layout**: `paths.procedural.connect(places)`, `.grid({...})`,
  `.circuit({...})` answer plans for `paths.fromPlan(plan)`.
- A railway is a one-lane, one-way edge (`lanes = { forward = 1, backward = 0 }`,
  `width = 3`); a footpath a narrow edge with no collider; a river course an
  edge whose surface is a water material, lowered with `lift`.

## Lanes, routes and AI drivers

Every edge carries directed lanes (`<edge>:+1` from its first node,
`<edge>:-1` back), joined through the nodes into a graph:

```luau
local route = net:route(car.position, goal, { by = "time" })     -- lanes, length, points
-- each tick: where the car is on its route, and the point it steers toward
local here = net:nearest(car.position, { heading = forward, lanes = route.lanes })
local target = net:lookAhead({ lane = here.lane, distance = here.laneDistance }, 15, { route = route.lanes })
```

A car built from `WheelCollider`s with `VehicleController { controlSource = "script" }`
drives a network by steering toward `target.position` each tick
(`setIntent { throttle, steer }`); traffic reads `lane.stopLine` and
`lane.speedLimit` from `net:lane(id)`.

`man pathNetwork` and the module's README list every call.
