Spling v1.2.0 | Lightweight Dynamic Spline (Constant movement/velocity without sampling!)

Spling v1.2.0 | Fast, Constant Velocity Splines

SplineDivider

- Links / Files:

Download class file:
SplineClass.rbxm (9.9 KB)

Download class test place:
SplineClassTestplace.rbxl (67.8 KB)

SplineDivider

- Introduction:

Hello everyone! :waving_hand:
→ I’ve been wanting to share a spline library I’ve been working on that is designed for smooth, controllable movement along a spline while keeping performance stable even when the spline changes at runtime.

The main goal of this library is to solve a long-time running problem with most spline libraries: controlled movement across a spline without relying on heavy sampling or large cached lookup tables. With this library, you can now move objects along curved paths mantaining their distance accurately, while still being able to edit the spline points at any moment without causing FPS drops or lag!

But there are already hundreds of spline libraries out there, what’s different about this one?

→ I noticed that most spline systems that support constant-speed movement need to sample the curve many times and cache those samples (usually around 1000 samples!). That works, but it can become expensive when the spline changes often, since the cached data has to be rebuilt each time.

This module avoids that problem by calculating spline length accurately with lightweight numerical methods instead of depending on dense sampling. When a point is changed, inserted, or removed, only the nearby affected curve arc lengths are recalculated, instead of recalculating the entire spline from scratch!

That makes it useful for dynamic splines where points may move or change during gameplay.

SplineDivider

- Documentation:

  • Where to start?

To make a spline you’ll need to provide the class an array of points that it will use to build the spline. You can also provide a table with a set of settings if you want to change how the spline will form.

local SplineClass = require(path.to.SplineClass)
local Points = {
	Vector3.new(0, 10, 0),
	Vector3.new(25, 5, -5),
	Vector3.new(50, 15, 5),
	Vector3.new(75, 5, 0),
}

--This table is optional, if you want to toy around with the values feel free to do so
local SplineSettings = { 
	Tension = 1, --How smooth/snappy the spline will look, default is 1
	Looped = false --Whenever the spline loops around itself or not, default is false
}

local Spline = SplineClass.new(Points, SplineSettings)

Spline:DrawDebug()

--This part of the code will create a new part and position it right at the middle of the spline
local MyPoint = Instance.new("Part")
MyPoint.Anchored = true
MyPoint.Size = Vector3.one * 3
MyPoint.Name = `SplinePoint`
MyPoint.Parent = workspace

MyPoint.CFrame = Spline:GetCFrame(0.5) --You can also archieve the same thing by setting it to Spline:GetCFrame(Spline.Length / 2, true)
  • Functions

GetPosition(alpha, isLength)
  • Description:
    This function will return a position (Vector3) given the alpha value (value between 0-1, 0-Length if isLength is true).
  • Typechecking:
GetPosition: (
	self: Spline,
	alpha: number,
	sLength: boolean?
) -> Vector3
  • Example:
local SplineClass = require(path.to.SplineClass)
local Points = {
	Vector3.new(0, 10, 0),
	Vector3.new(25, 5, -5),
	Vector3.new(50, 15, 5),
	Vector3.new(75, 5, 0),
}

local Spline = SplineClass.new(Points)
local MyPoint = workspace.SplinePoint :: Part --example part here

MyPoint.Position = Spline:GetPosition(Spline.Length * 0.5, true)
GetCurve(alpha, isLength)
  • Description:
    Returns the internal Bézier curve at the given position along the spline, together with the local alpha of that position inside the returned curve.

  • Typechecking:

GetCurve: (
	self: Spline,
	alpha: number,
	isLength: boolean?
) -> (BezierCurve, number)
  • Example:
local SplineClass = require(path.to.SplineClass)

local Points = {
	Vector3.new(0, 10, 0),
	Vector3.new(25, 5, -5),
	Vector3.new(50, 15, 5),
	Vector3.new(75, 5, 0),
}

local Spline = SplineClass.new(Points)

local Curve, LocalAlpha = Spline:GetCurve(0.5)

print("Curve:", Curve)
print("Local alpha:", LocalAlpha)

print("Start:", Curve.P0)
print("End:", Curve.P3)
print("Curve length:", Curve.Length)
GetClosestWorldPoint(worldPosition, samples, iterations)
  • Description:
    Returns the closest position on the spline to a given world position. It returns both the closest Vector3 position and its normalized spline alpha.

samples and iterations are optional and default to 50 and 12 respectively. Increasing them can improve precision at the cost of additional computation.

  • Typechecking:
GetClosestWorldPoint: (
	self: Spline,
	worldPosition: Point,
	samples: number?,
	iterations: number?
) -> (Vector3, number)
  • Example:
local SplineClass = require(path.to.SplineClass)

local Spline = SplineClass.new({
	Vector3.new(0, 0, 0),
	Vector3.new(25, 10, 10),
	Vector3.new(50, 0, 0),
})

local Target = workspace.Target

local ClosestPosition, Alpha = Spline:GetClosestWorldPoint(Target.Position)

print("Closest position:", ClosestPosition)
print("Spline alpha:", Alpha)

Target.Position = ClosestPosition

You can also change the amount of samples and refinement iterations:

local Position, Alpha = Spline:GetClosestWorldPoint(
	workspace.Target.Position,
	100,
	16
)
GetClosestLocalPoint(alpha, isLength)
  • Description:
    Returns the closest spline control point surrounding a given position along the spline.

Unlike GetClosestWorldPoint(), this does not search for a position along the spline. Instead, it gets the Bézier curve at the provided alpha and returns whichever endpoint of that curve is closest according to its local alpha.
It returns the local curve alpha and the Bézier curve containing the requested position.

  • Typechecking:
GetClosestLocalPoint: (
	self: Spline,
	alpha: number,
	isLength: boolean?
) -> (Vector3, number, BezierCurve)
  • Example:
local SplineClass = require(path.to.SplineClass)

local Spline = SplineClass.new({
	Vector3.new(0, 0, 0),
	Vector3.new(25, 10, 0),
	Vector3.new(50, 0, 0),
	Vector3.new(75, 10, 0),
})

local Point, LocalAlpha, Curve = Spline:GetClosestLocalPoint(0.6)

print("Closest control point:", Point)
print("Local curve alpha:", LocalAlpha)
print("Curve:", Curve)
GetCurvePointAlpha(position, isLength)
  • Description:
    Returns the spline alpha corresponding to a spline curve point along with its Bézier curve.

The supplied position must correspond exactly to the starting point (P0) of one of the spline’s Bézier curves.

If the provided position does not correspond to a curve point, the function returns nil.

  • Typechecking:
GetCurvePointAlpha: (
	self: Spline,
	position: Point,
	isLength: boolean?
) -> (number?, BezierCurve?)
  • Example:
local SplineClass = require(path.to.SplineClass)

local Spline = SplineClass.new({
	Vector3.new(0, 0, 0),
	Vector3.new(25, 10, 0),
	Vector3.new(50, 0, 0),
	Vector3.new(75, 10, 0),
})

local Alpha, Curve = Spline:GetCurvePointAlpha(Spline.Points[2])

if Alpha then
	print("Point alpha:", Alpha)
	print("Curve:", Curve)
end

You can retrieve its distance along the spline instead:

local Distance = Spline:GetCurvePointAlpha(Spline.Points[2], true)

print("Distance from start:", Distance)
GetCFrame(alpha, isLength)
  • Description:
    Returns a CFrame at the specified position along the spline.

The position is calculated using the same constant-distance system as GetPosition(), while the orientation follows the tangent/direction of the spline at that point. This makes this function specifically useful for objects such as trains, projectiles, cameras, vehicles, or anything else that should automatically face along the spline.

  • Typechecking:
GetCFrame: (self: Spline, alpha: number, isLength: boolean?) -> CFrame
  • Example:
local SplineClass = require(path.to.SplineClass)

local Spline = SplineClass.new({
	Vector3.new(0, 10, 0),
	Vector3.new(25, 5, -5),
	Vector3.new(50, 15, 5),
	Vector3.new(75, 5, 0),
})

local MyPart = workspace.MyPart

MyPart.CFrame = Spline:GetCFrame(0.5)

It can also be used with distance:

MyPart.CFrame = Spline:GetCFrame(25, true)
SetPoint(point, index)
  • Description:
    Changes an existing spline point at the provided index.

Only the nearby Bézier curves affected by the changed point are recalculated, instead of rebuilding the entire spline. This is particularly useful for dynamic splines where control points move during runtime.

  • Typechecking:
SetPoint: (
	self: Spline,
	point: Vector3 | Instance | CFrame,
	index: number
) -> ()
  • Example:
local SplineClass = require(path.to.SplineClass)

local Spline = SplineClass.new({
	Vector3.new(0, 0, 0),
	Vector3.new(25, 0, 0),
	Vector3.new(50, 0, 0),
	Vector3.new(75, 0, 0),
})

Spline:SetPoint(
	Vector3.new(25, 25, 0),
	2
)

Spline:DrawDebug()

You can also use the position of a part:

Spline:SetPoint(workspace.ControlPoint, 2)
InsertPoint(point, index)
  • Description:
    Inserts a new control point into the spline.

If an index is provided, the point will be inserted at that position in the point array. After insertion, the affected Bézier curves and spline length are recalculated.

  • Typechecking:
InsertPoint: (
	self: Spline,
	point: Point,
	index: number?
) -> ()
  • Example:
local SplineClass = require(path.to.SplineClass)

local Spline = SplineClass.new({
	Vector3.new(0, 0, 0),
	Vector3.new(50, 0, 0),
	Vector3.new(75, 0, 0),
})

Spline:InsertPoint(
	Vector3.new(25, 15, 0),
	2
)

Spline:DrawDebug()

This changes the point array from:

{
	Vector3.new(0, 0, 0),
	Vector3.new(50, 0, 0),
	Vector3.new(75, 0, 0),
}

Into:

{
	Vector3.new(0, 0, 0),
	Vector3.new(25, 15, 0),
	Vector3.new(50, 0, 0),
	Vector3.new(75, 0, 0),
}
RemovePoint(index)
  • Description:
    Removes a control point from the spline at the provided index.

After removing the point, the affected nearby curves are recalculated and Spline.Length is updated automatically.

  • Typechecking:
RemovePoint: (self: Spline, index: number) -> ()
  • Example:
local SplineClass = require(path.to.SplineClass)

local Spline = SplineClass.new({
	Vector3.new(0, 0, 0),
	Vector3.new(25, 15, 0),
	Vector3.new(50, 0, 0),
	Vector3.new(75, 0, 0),
})

Spline:RemovePoint(2)

Spline:DrawDebug()
DrawDebug(samples)
  • Description:
    Draws a visual representation of the spline inside Workspace.

Red points are created along the spline while green segments connect the samples together, making it easier to visualize the shape of the spline while developing.

samples controls how many subdivisions are used to display the spline and defaults to 100.

This function is intended for debugging and visualization.

  • Typechecking:
DrawDebug: (self: Spline, samples: number?) -> ()
  • Example:
local SplineClass = require(path.to.SplineClass)

local Spline = SplineClass.new({
	Vector3.new(0, 0, 0),
	Vector3.new(25, 15, 0),
	Vector3.new(50, -10, 0),
	Vector3.new(75, 0, 0),
})

Spline:DrawDebug()

You can provide your own sample amount:

Spline:DrawDebug(200)

Calling DrawDebug() again will update the existing debug visualization rather than requiring you to manually remove it first.

Spline:SetPoint(Vector3.new(25, 30, 0), 2)
Spline:DrawDebug()
CreateSplineLUT(Samples)
  • Description:
    This function will sample the spline and create a LUT that will be used by every function to save on performance in exchange of dynamicness, accuracy and memory. This function should be used once you are sure that the spline won’t be updated in the future or often. The performance gain usually sits at around 20-30% so don’t expect much :cat_face:.

Note: If any points are updated/inserted/removed, the spline will automatically disable the LUT. You will have to call this function again to recalculate the LUT.

  • Typechecking:
CreateSplineLUT: (
	self: Spline,
	Samples: number?
) -> (),
  • Example:
local SplineClass = require(workspace.SplineClass)

local Spline = SplineClass.new({
	Vector3.new(0, 0, 0),
	Vector3.new(25, 10, 0),
	Vector3.new(50, 0, 0),
	Vector3.new(75, 10, 0),
})

local Start = os.clock()
Spline:GetPosition(0.5)
print(`Without LUT:	{os.clock() - Start}s`)

Spline:CreateSplineLUT() --If no sample number is given, default one will be used

local Start = os.clock()
Spline:GetPosition(0.5)
print(`With LUT:	{os.clock() - Start}s`)
RemoveDebug()
  • Description:
    Removes the debug visualization created by DrawDebug() for this spline.

This does not modify or destroy the spline itself.

  • Typechecking:
RemoveDebug: (self: Spline) -> ()
  • Example:
local SplineClass = require(path.to.SplineClass)

local Spline = SplineClass.new({
	Vector3.new(0, 0, 0),
	Vector3.new(25, 15, 0),
	Vector3.new(50, 0, 0),
})

Spline:DrawDebug()

task.wait(5)

Spline:RemoveDebug()
Destroy()
  • Description:
    Destroys the spline object and cleans up its associated data.

This automatically removes its debug visualization and clears the spline’s points, Bézier curves, settings, and internal values.

The spline should no longer be used after calling this function.

  • Typechecking:
Destroy: (self: Spline) -> ()
  • Example:
local SplineClass = require(path.to.SplineClass)

local Spline = SplineClass.new({
	Vector3.new(0, 0, 0),
	Vector3.new(25, 15, 0),
	Vector3.new(50, 0, 0),
})

Spline:DrawDebug()

--When you're finished with the spline:
Spline:Destroy()

SplineDivider

- Drawbacks:

→ The only noticeable drawback that you’ll get from this module is that calculating the position along the spline will be minimally slower compared to other spline modules. However, I’ve been trying it out myself and the impact seems negligible, you’re welcome to benchmark it if you’d like and share your results!

SplineDivider

- Final notes / Summary:

This module is mainly focused on giving developers accurate spline movement without the usual performance cost of heavy sampling. You still get length-based control, alpha-based control, runtime editing, and orientation.

I’d love to hear feedback, suggestions, or possible improvements! :cat_face:

26 Likes

Update

  • Fixed some typechecking errors
  • Updated :Destroy() method

Update

  • Added more control over the Tension setting
  • Fixed some bugs (i forgot which ones)

Idk what else to add to this so I wont update it anymore until something comes up

Update

  • Fixed some of the logic for calculating spline points (should look better now)
  • Added a new video and test place to the main post to try out the class :steamhappy:
    i think i added more stuff but i can’t remember!

this is really sick, great job man
what’s the performance like?

1 Like

From what I tested it’s really performant, each :GetPosition() call takes around ~4.5 microseconds (0.0000045 seconds)

2 Likes

Update

  • Optimized the :DrawDebug() method
  • Fixed some recalculation of arc-length issue

Update

  • Added PathWaypoint support
  • Redid tension so it’s more distance dependant to avoid overshooting

Update

  • Fixed some typechecking issues
  • Fixed a bug with the :Destroy() method
  • Added more input sanity checks
2 Likes

How would I get the Length based on a certain Point? (And also the other way around)
I am trying to do a custom Climb System and I need that part so I can accurately adjust the Climbing Paths and move/jump/transition from 1 Path to another

1 Like

Hello! Sorry for the delay in my response, from what I understand do you mean getting the distance/alpha along the spline from a world position? So for example GetPosition(25, true) gives you the position 25 studs along the spline, and you want the inverse where supplying a Vector3 returns that 25 stud distance?

No I am talking about getting Progress/Alpha along the Spline, that it takes to reach one of its Points, like the second Point for example.

And vise versa; getting the closest Point or THE Point, given a certain Alpha/Progress along the Spline.
So for example, my Point 2 is on Alpha 0.3, and I do like Spline:GetPointByAlpha(0.29) (something like that), that it would return the number “2”, aka the Index of the Point.

With I refer to the number Index from the Vector3s located inside of the “Points” table/Array.

I hope I worded it correctly

1 Like

Ah i see what you mean, yeah my bad this kind of behaviour wasn’t implemented, I published a new change to the class with 3 new functions:

First one:

SplineClass:GetClosestLocalPoint(_alpha, _isLength): (Vector3, Alpha, Curve)

This function can be used for what you mean for getting the closest point given a certain alpha/progress along the spline

Second one:

SplineClass:GetCurvePointAlpha(_Position: Point, _isLength: boolean): (Alpha, Curve)

This function will look for the point you inputed inside the spline and will return the alpha/distance of said point. Take into account that this point must be one that exists as a control curve point. If you want to get the closest point to a spline and its alpha use the next function

Third one

SplineClass:GetClosestWorldPoint(_WorldPosition: Point, _Samples: number?, _Iterations: number?): (Position, Alpha)

This function was already planned to be added but was left as a WIP, i just finished it. What it does is basically it takes in a position of anywhere in the workspace and it will try to find the point in the spline that is closest to said position.

I think thats all, let me know if you need anything else :cat_face:

1 Like

Thanks, thats exactly what I meant, but the PackageLink does not work; it does not update automatically - it gives an error and such, could you post the updated version here perhaps?
I assume the asset from the PackageLink isn’t public or whatever

1 Like

Oh wow, I thought it updated for everyone :sob:
my bad here’s the latest version:
SplineClass.rbxm (8.9 KB)

1 Like

Thanks, it updates now properly, no error

1 Like

Ah, sorry, I didn’t actually take an actual look/proper at the functions, I assumed that you understand what I meant based off of the function names, my bad.

I meant a Function that gets/returns the Alpha, based off of a given Point/Index (the number), which is (poorly) visualized below, with “B” in the top right:

And a function that gets the closest Point (or the closest 2 I guess) (it returns the Indexes of the Points, which are numbers), relative to the Alpha, which is (also poorly) visualized below, and marked with “A” in the top right:

I hope this cleared things up a bit

I know that I could technically get the Index of the Point through the Vector3, but it could happen that there are 2 Points with the same Position/Vector3, thats why I am asking if you could make a function that works with Index (number) instead.

I’m so glad that this exists, I was trying to achieve this kind of behaviour with my own spline library but was struggling because I was using lookup tables. I did complicate things by covering many curve & spline types but a Catmull-Rom spline is used for most applications.

1 Like

I see, I think I still don’t understand though since with image B you could use the SplineClass:GetClosestWorldPoint(Position) function which will literally return to you the closest point to the spline along with the alpha. For image B if you want the closest points to the alpha I think what you want is the curve, you can use the function SplineClass:GetCurve(Alpha) which will return a table composed of the points that form the curve, in said curve the points you are looking for are P0 and P3.

Let me know if im wrong tho I’m happy to help :cat_face:

2 Likes