Bhristt's Bezier Curve Module (Tween support)

Introduction


I’ve created a module that allows you to create an nth degree Bezier Curve given Vector3s and BaseParts (to add points that can change as the BaseParts position changes). This module can return the position of a point at any interval t, where 0 ≤ t ≤ 1, and is able to return the derivative of the Bezier Curve at any interval t, where 0 ≤ t ≤ 1. This is especially useful when you want to create something like an arrow following a path. The arrow has to point in the direction of the tangent vector at a point, so this module allows that to happen.

I would also like to add that this module uses the explicit definition of a nth degree Bezier Curve to get a point at t. It creates a sum of the Bernstein basis polynomials of degree n, where n is the degree of the Bezier Curve. Getting the derivative of this explicit definition, you arrive with the formula that gives you the derivative of the Bezier Curve at point t.

Most modules here on the dev forum use the recursive definition of the Bezier Curve, which, although it is reliant and accurate, is slower and less versatile.

Usage

Usage


Usage of this module is very simple, first create a Bezier object with the constructor, and add points to the Bezier. Then, use the functions Bezier:CalculatePointAt(t) and Bezier:CalculateDerivativeAt(t). The constructor can take multiple Vector3 and BasePart inputs, so creating a Bezier with points is very easy.

Constructor:

local Bezier = require(script:WaitForChild("Bezier"))
local NewBezier = Bezier.new(workspace.P1, workspace.P2, workspace.P3, workspace.P4)

-- constructor: Bezier.new(...: Vector3 | BasePart): BezierObject
-- this creates a 4th degree Bezier (otherwise known as a Cubic Bezier Curve)

The constructor can take in multiple arguments, so you can create a nth degree Bezier right off the bat.

Properties:

print(NewBezier.Length) --  the length of the Bezier Curve created
print(NewBezier.LengthIterations) -- the number of times to iterate through the curve to get a length (higher the number, the higher the accuracy)

Methods:

NewBezier:AddBezierPoint(Point: Vector3 | BasePart, Index: number?)

-- adds a point to the Bezier object at the given index position
-- if an index is not given, it'll add the point at the end of the list of points
NewBezier:ChangeBezierPoint(Index: number, NewPoint: Vector3 | BasePart)

-- changes the point at the index to the given point
-- if the current point at the index is not found, then this will prompt an error
NewBezier:GetPoint(index: number): Vector3?

-- returns the point of the Bezier at the given index
-- if the point is not found, this will error
NewBezier:RemoveBezierPoint(index: number)

-- removes the BezierPoint at the given index
-- will not error if there isn't a BezierPoint at the index, but it really only works if there is
NewBezier:UpdateLength()

-- updates the bezier curve's length
-- function does not need to be called, the object does this automatically
NewBezier:CalculatePositionAt(t: number): Vector3

-- gets the position of the Bezier Curve at t
-- only works if there is at least 1 point in the Bezier
NewBezier:CalculatePositionRelativeToLength(t: number): Vector3

-- gets the position of the BezierCurve at t, relative to the Curve's length
-- only works if there is at least 1 point in the Bezier
-- t must be a percentage number between 0 and 1, referring to the percentage length of the curve
NewBezier:CalculateDerivativeAt(t: number): Vector3

-- gets the derivative of the Bezier Curve at t
-- only works if there are at least 2 points in the Bezier
NewBezier:CalculateDerivativeRelativeToLength(t: number): Vector3

-- gets the derivative of the Bezier Curve at t, relative to the Cuve's length
-- only works if there are at least 2 points in the Bezier
-- t must be a percentage number between 0 and 1, referring to the percentage length of the curve
NewBezier:CreateVector3Tween(object: Instance | {[any]: any}, propertyTable: {any}, TweenInfo, RelativeToLength: boolean?): Tween

-- tween an object's Vector3 property through the Bezier
-- the object does not have to be an Instance, it can also be a regular table
-- the object's properties will tween as long as you give the correct property indeces in the propertyTable
-- optional boolean at the end to make the tween relative to the length of the curve, or parameter t
NewBezier:CreateCFrameTween(object: Instance | {[any]: any}, propertyTable: {any}, TweenInfo, RelativeToLength: boolean?): Tween

-- tween an object's CFrame property through the Bezier
-- the object does not have to be an Instance, it can also be a regular table
-- the object's properties will tween as long as you give the correct property indeces in the propertyTable
-- optional boolean at the end to make the tween relative to the length of the curve or parameter t
Examples

Examples


Example 1

The following code produces a visual Bezier Curve in the workspace, controlled by the green parts (in the video). The parts are labeled P1, P2, P3, and P4 in order of the indeces of the points in the Bezier.

local Bezier = require(script:WaitForChild("Bezier"))
local NewBezier = Bezier.new(workspace.P1, workspace.P2, workspace.P3, workspace.P4)

local NumPoints = 20
local Points = {}
for i = 1, NumPoints do
	local TargetPart = Instance.new("Part", workspace)
	TargetPart.Size = Vector3.new(0.75, 0.75, 3)
	TargetPart.BrickColor = BrickColor.new("Really red")
	TargetPart.CanCollide = false
	TargetPart.Anchored = true
	table.insert(Points, TargetPart)
end
local Lines = {}
for i = 1, NumPoints - 1 do
	local TargetPart = Instance.new("Part", workspace)
	TargetPart.Size = Vector3.new(0.5, 0.5, 1)
	TargetPart.BrickColor = BrickColor.new("Black")
	TargetPart.CanCollide = false
	TargetPart.Anchored = true
	table.insert(Lines, TargetPart)
end


while wait() do
	for i = 1, #Points do
		local t = (i - 1) / (#Points - 1)
        -- calculates the position and derivative of the Bezier Curve at t
		local position = NewBezier:CalculatePositionAt(t)
		local derivative = NewBezier:CalculateDerivativeAt(t)
        -- sets the position and orientation of the point based on the 
        -- position and derivative of the Bezier Curve
		Points[i].CFrame = CFrame.new(position, position + derivative)
	end
	for i = 1, #Lines do
		local line = Lines[i]
		local p1, p2 = Points[i].Position, Points[i + 1].Position
		line.Size = Vector3.new(line.Size.X, line.Size.Y, (p2 - p1).Magnitude)
		line.CFrame = CFrame.new(0.5 * (p1 + p2), p2)
	end
end

Running this code in studio, we get the following:

Example 2

The previous example created a visual display for the Bezier that was made. Using this, we can now show how the Bezier Module can tween objects within the Bezier.

The following code creates two Tween objects using Bezier:CreateVector3Tween() and Bezier:CreateCFrameTween(), each acting on a part I named “GlowPart” parented to workspace. This works like a regular Tween.

local GlowPartTweenInfo = TweenInfo.new(15, Enum.EasingStyle.Sine, Enum.EasingDirection.Out, 0, false, 0)
local Tween = NewBezier:CreateVector3Tween(workspace.GlowPart, {"Position"}, GlowPartTweenInfo)
local Tween2 = NewBezier:CreateCFrameTween(workspace.GlowPart, {"CFrame"}, GlowPartTweenInfo)
Tween2:Play()

Running this in studio, we get the following:

Notice that as I move the control points in the Bezier, the path of the GlowPart also changes even though the Tween is playing. The path will constantly change as the control points are being changed, and the CFrame of the GlowPart changes accordingly.

Relative to Bezier Curve Length

Relative to Bezier Curve Length


You may be wondering what it means for the positions and derivatives of the Bezier Curve to be relative to the length of the Bezier Curve. This section provides an explanation of that.

Suppose you have a Bezier Curve made of 5 control points. The curve looks something like this:

As points are created within that curve, some points in the curve may lie very close to other points even if those points are spread at equal intervals. This means that, the point Bezier:CalculatePositionAt(0.25) is not always the same as the point at 25% of the Bezier Curve’s length. You can see this visually here:

I placed 100 points within this curve equally spread in the interval t. The variable t goes from 0 - 1 at an interval of 1/100. However, you can see that these points are not equally spread out. They become more compact in areas where the control points seem to go backwards in order.

Now this prompts the question, “how do we fix this?” Well, fixing this problem is easy now that we can easily calculate the derivative, we are able to calculate the arc length of the Bezier Curve using an approximation:

where l is the length of the Bezier Curve, r’(t) is the derivative of the Bezier Curve at t, and n is the number of iterations (for accuracy purposes).

Since we now have the arc length of the Bezier Curve, we can find values of t in the Bezier Curve where it returns a position where the length of the curve from the beginning of the curve to the position at t is equal to the given percentage of the length of the curve. Doing this, we end up with this result:

Now, all the points are equidistant from each other despite the fact that some control parts are closer and further away from each other. The points are being returned based on the percentage of the length of the curve. To give a visual for the effect that the RelativeToLength parameter gives in the functions Bezier:CreateVector3Tween() and Bezier:CreateCFrameTween(), I will tween two objects.

In the video above, the blue neon block is the tween relative to the length of the bezier, and the yellow neon block is the regular tween in the Bezier Curve. You can see that the yellow block slows down at the part where all the points are clamped up together, and speeds up at the points where they aren’t. The blue block moves at a constant speed throughout the entire curve.

Updates

October 25, 2021


  • Organized module
  • Removed global functions
  • Fixed bugs that would error on “Back” and “Elastic” easing styles
  • Added hidden _connections property to Bezier objects that contains connections made with BaseParts

Summary


The Bezier module is up for grabs, you can get it here:

Hopefully this helps you in any of your Bezier Curve needs!
(If you want more information on the math of Bezier Curves, you can see more here)

162 Likes

From the video, I can assume that it updates in real time?

2 Likes

Yes, setting the points to BaseParts and moving the BaseParts will change the values of the points in the Bezier object. So when you call the function Bezier:CalculatePositionAt(t), the position will be updated based on the position of the points.

1 Like

Thank you
This saves me a lot of headache

2 Likes

Update: October 16, 2021

  • Added support for tweening within the path created by a Bezier object.
  • Added Vector3 tween and CFrame tween of an Instance / table.

Update: October 18, 2021

  • Added a ‘Length’ property to created Beziers which gives the length of the Bezier Curve created by the Bezier object
  • Added function for getting a position of the Bezier Curve relative to the length of the Bezier, and a function for getting the derivative of the Bezier Curve relative to the length of the Bezier.
  • Added RelativeToLength parameter in the Tweening functions (see Relative to Bezier Curve Length section)

Nice module, would be even nicer if you added something like easing styles

1 Like

The module does have easing styles. The tweening functions take in a TweenInfo object, which you can see more information on here:

This module tweens exactly like how TweenService tweens things, just without using TweenService:Create(). Instead you use NewBezier:CreateCFrameTween() or NewBezier:CreateVector3Tween()

To see more information on these functions, check the usage category of the post.

2 Likes

Thanks for the epic module, it’s the best I found :smiley:

I have just some suggestions:

  • Convert global functions into local.
  • Bezier.new parameter would be better with a table instead of variadic args.

I’ll showcase your module in my channel with the mentioned edits with the proper credits :smile:

Otherwise, I wonder how to link 2 curves together using your module :thinking:

1 Like

Thanks, I’m glad you liked the module!

Originally I was going to have all the functions be local functions, but the functions are all used in multiple functions within the object created from the constructor. It would make the code even more unorganized if I did make the functions local functions, since I would have to copy and paste the functions within the object functions. What I was thinking of doing was putting all of the functions in a separate module, so that they can be accessed without taking any space in the main module script.

The constructor Bezier.new() does take in variadic arguments only for ease of use, but if using a table instead of giving arguments would be better I can change it to that!

I was eventually going to add a function to link two Bezier curves together, but I’ve been so busy with school! This will definitely be added though!

1 Like

if possible can you send me a message in discord:
CodesOtaku#8327

1 Like

Ive been looking for something like this for months, thank you!!

1 Like

Do you know how i could basically create example 1 but instead of the black part have a model tweened along it instead?

Thank you for this module! Very helpful to not-so-advanced scripters who want to create something fun without having to learn advanced scripting/math.

1 Like

If I run this code, let’s say, a solid 30 times, If I move one of the points on the bezier I experience severe lag. Is this normal?

local NewBezier = Bezier.new(workspace.P1, workspace.P2,workspace.P3)
		
		
		local Trail = workspace.Trail:Clone()
		Trail.Parent = Folder
		
		
		local Info = TweenInfo.new(.25,Enum.EasingStyle.Quad,Enum.EasingDirection.InOut)
		
		local Tween = NewBezier:CreateVector3Tween(Trail,{"Position"},Info)
		Tween:Play()
		
		
		task.wait(.5)
		Tween:Destroy()
		Trail:Destroy()

Absolutely amazing module! Love it can tell all of the hard work put into this!

nice, btw is there a function that which evenly distributes the points
Bezier8

1 Like

Yes there is. You can achieve this by using

NewBezier:CalculatePositionRelativeToLength(number)

For more information on the module, check the usage and example section!

1 Like

thank you, and also i liek ur module, its cool btw

Nice module! Would be cool if you could include degree elevation, subdivision and normal vectors.

De Casteljau’s algorithm is actually much faster than the explicit definition, you can try it yourself:

local points = {Vector3.new(0,1.324,38), Vector3.new(9.28,0,2),
	Vector3.new(0,0.38292,4.372), Vector3.new(0.32762,78.281,9)}

local function deCasteljau(t:number):Vector3
	local copy = {unpack(points)}
	local n = #copy
	for j = 1, n - 1 do
		for k = 1, n - j do
			copy[k] = copy[k] * (1 - t) + copy[k + 1] * t
		end
	end
	return copy[1]
end

local function fac(n:number):number
	if n == 0 then return 1 end
	return n * fac(n - 1)
end

local function explicit(t:number):Vector3
	local result = Vector3.zero
	local n = #points-1
	for i = 0, n do
		result += (fac(n)/(fac(i)*fac(n-i))) * t^i * (1-t)^(n-i) * points[i+1]
	end
	return result
end
1 Like