How do I tween along a curve? A look at Bézier curves

Introduction

Ever since I was introduced to curves (or arcs, rather), like in a well-made bow weapon where you can see the arc and end destination of the projectile, or missiles that came from rockets, I was always interested in learning how it works, but always thought of it as super complicated. After years of casually wondering but never diving deep, I saw someone mention Bézier curves. I was bored, so I got right into it. It seemed complicated at first, but it really wasn’t.

How do they work?

Put simply, a quadratic Bézier curve goes from P₀ to P₂ while being “pulled” to a third point, P₁, using linear interpolation. If you’re not familiar with linear interpolation, see below:

Interpolation

Imagine a point A and a point B, at any distance from one another. Now, imagine the shortest straight line that can connect the two. This line can be of any length, but let’s assume it’s 20 units long. Let’s treat point A as the starting point, and point B as the goal.

For now, assume we have a lerp(a, b, p) function that interpolates a towards b at an alpha of p.

If you’re familiar with percentages, you’ll know that 0% through 100% can be represented as 0 through 1. Let’s take a percent of 50, which can be represented as an alpha of 0.5. Assume we execute and print the lerp function passing in point A, point B, and 0.5:
lerp(A, B, 0.5)
It would return to us 10 units. Why 10 units? Because 10 units is 50%, or 0.5, of the way from point A to point B. If we passed in 0.2, we would get 4 units, because that is 20% of the way from point A to point B. If we passed in 0.8, we would get 16 units, because that is 80% of the way. This is how ROBLOX’s Lerp function works, they just apply it to each respective component:

Vector3.new(X, Y, Z):Lerp(Vector3.new(A, B, C), 0.5)

Would lerp from X to A at 0.5, from Y to B at 0.5, and from Z to C at 0.5, and would return a Vector3.new() with the new respective interpolated values. This new Vector3 is the point in space 50% of the way from the first vector to the second.

Did this help you understand interpolation?

  • Yes
  • No

0 voters

Linear interpolation takes 0 through 1. In this case, we will use t which is 0.25. What we observe above is that this number t is used in 3 interpolations. First, it interpolates from P₀ to P₁, at an alpha of t, and positions a new point, Q₀, at the result. Then, it interpolates from P₁ to P₂, using t, creating a new point, Q₁.
Finally, it interpolates from Q₀ to Q₁, using t, which creates the final point, B.

So, in other words:
Q₀ is positioned 25% of the way from P₀ to P₁
Q₁ is positioned 25% of the way from P₁ to P₂
B is positioned 25% of the way from Q₀ to Q₁

Bézier_2_big

Repeat that many times over increments of 0.02, and the animation above is what we get.

It is useful to note that P₁ is what’s known as an anchor point. A Bézier curve can contain several anchor points to create more intricate curves. You can read more about that on the Wikipedia.

How do I implement this?

Thankfully, ROBLOX has already given us a :Lerp() function that is a method of many data types, like CFrame and Vector3, which is what we will be using.

All we need to do for this example is to have three Vector3s and a ball. These will act the same as the points shown above. I will use four balls and their positions to act as the points.

07ed0c984c1a11432c018260bf9559cd

The blue ball, named “From”, acts as P₀. The green ball, “To”, acts as P₂. The red ball, “Anchor” acts as the anchor point P₁. The black ball, “Base”, acts as the main point B. The gray balls are Q₀ and the white ones are Q₁. The red balls represent every interpolation between Q₀ and Q₁.

local to = workspace:WaitForChild("To")
local from = workspace:WaitForChild("From")
local base = workspace:WaitForChild("Base")
local anchor = workspace:WaitForChild("Anchor")

local message = Instance.new("Message", workspace)
local waitTime = 9

for i = 0, waitTime, 1 do
	message.Text = "Waiting... "..waitTime-i.." seconds left"
	wait(1)
end
message:Destroy()

function ball(position, color)
	local part = Instance.new("Part")
	part.Shape = "Ball"
	part.Size = Vector3.new(1,1,1)
	part.BrickColor = color
	part.Material = Enum.Material.SmoothPlastic
	part.Anchored = true
	part.Position = position
	part.Parent = workspace
end

for i = 0, 1, 0.03 do
	local interpolation_1 = from.Position:Lerp(anchor.Position, i)
	local interpolation_2 = anchor.Position:Lerp(to.Position, i)
	local interpolation_3 = interpolation_1:Lerp(interpolation_2, i)
	ball(interpolation_3, BrickColor.Red())
	ball(interpolation_1, BrickColor.Gray())
	ball(interpolation_2, BrickColor.White())
	base.Position = interpolation_3
	wait(0.01)
end

It takes 3 seconds to complete. Take this with a grain of salt, but I think that is because since it takes 1 second to interpolate each point to its destination, they compound. So three interpolations are three seconds, five interpolations are five seconds. If you want it to take more time, you should make the second argument of the for loop the amount of seconds you want, instead of 1. You then must set the increment to 0.01*x, x here being the number of interpolations you’re doing at once, which can increase if you decide to have more anchor points. Then, instead of using just i as the alpha in the :Lerp() methods, use i/seconds, seconds being the value you changed the second argument.

It is important to note that the time each interpolation takes can vary. This can amount to inaccuracy. For example, I tested it with seven seconds, and found that it actually took 7.8 seconds. I suggest you take this into account if time precision is important to you.

file:
bezier.rbxl (35.2 KB)

Thank you so much for reading!

Did this help you?

  • I understood these concepts well.
  • The explanation could’ve been better, but it still helped.
  • The explanation was bad and didn’t help me.

0 voters

28 Likes

It has been very helpful!!! Thank you!!!

2 Likes