Need help understanding Bezier Curves

oh… well as long as it works, nevermind that.

anyways, this was the problem I was talking about earlier. it’s because the loops in lua don’t include the last number, which I wasn’t sure about. so you have to change 1 to 1.01.

the extra decimal digits at the end are just small division errors, ignore them.

yeah i tried this earlier, it just breaks the script (it fails to find the nearest point thing)

decided to take it into my own hands so we can finally end this thread… lol

the code was actually sound, it seems like lua had some comparison error for really small decimals. not sure what it was, but for some reason it said that length != points[#points].Distance… even though that’s exactly what we set it to!

so, instead I made it so that if the target didn’t find a matching point, it uses the final point by default. here’s the product. now the final point is included.

image

local ROOTS = {
	Vector3.new(10, 10, 0),
	Vector3.new(10, 40, 0),
	Vector3.new(40, 10, 0),
	Vector3.new(40, 40, 0)
}

for _, position in pairs(ROOTS) do
	local part = Instance.new("Part")
	part.BrickColor = BrickColor.new("Bright blue")
	part.Size = Vector3.new(0.5, 0.5, 0.5)
	part.Position = position
	part.Anchored = true
	part.Transparency = 0.5
	part.Parent = workspace
end

-- PLOTTING --
function bezier3(n, p0, p1, p2, p3)
	local d = 1 / n

	local function lerp(p0, p1, t)
		return (1 - t) * p0 + t * p1
	end

	local function quadratic(p0, p1, p2, t)
		local L1 = lerp(p0, p1, t)
		local L2 = lerp(p1, p2, t)
		return lerp(L1, L2, t)
	end


	local function cubic(p0, p1, p2, p3, t)
		local Q1 = quadratic(p0, p1, p2, t)
		local Q2 = quadratic(p1, p2, p3, t)
		return lerp(Q1, Q2, t)
	end
	
	local points = {}
	points[1] = {
		["Position"] = p0, -- starting point
		["Distance"] = 0
	}

	for t = d, 1 + d, d do
		local position = cubic(p0, p1, p2, p3, t) -- get unweighted position
		local previous = points[#points] 
		local gap = (position - previous.Position).Magnitude
		local distance = previous.Distance + gap

		-- store the vector3 as position, total distance as distance
		-- and distance from the previous point as gap
		points[#points+1] = {
			["Position"] = position,
			["Distance"] = distance,
			["Gap"] = gap
		}
	end

	-- total curve length
	local length = points[#points].Distance
	local bezier = {}

	for t = d, 1 + d, d do
		local target = length * t

		-- find the first point that is >= target and the one before it
		-- that means the target length is in between these two points
		local p0, p1
		for i, point in pairs(points) do
			if point.Distance >= target then
				p0 = points[i-1]
				p1 = point
				break
			else
				-- use the final point if target didn't match
				p0 = points[#points-1]
				p1 = points[#points]
			end
		end

		bezier[#bezier+1] = lerp(
			p0.Position,
			p1.Position,
			(target - p0.Distance) / p1.Gap
		)
	end

	return bezier
end

for _, position in pairs(bezier3(20, table.unpack(ROOTS))) do
	local part = Instance.new("Part")
	part.BrickColor = BrickColor.new("Bright red")
	part.Size = Vector3.new(0.4, 0.4, 0.4)
	part.Position = position
	part.Anchored = true
	part.Parent = workspace
end

by the way, I’m not sure why you said points[1] broke your script. worked okay with me.

1 Like

Im confused why you put roots thing outside of the function and not change the roots every time depending on what points are passed in the function. Also in the for loop that calls the function, where did you get 20 from

this is what I did. roots is just a set of positions, I used them to place blue blocks and also as function parameters. see the line bezier3(20, table.unpack(ROOTS)).

I added it as an extra parameter, that allows you to specify how many points to add. up until now we’ve been using 100 points, in the picture I only added 20 points because you couldn’t tell the difference between weighted and unweighted.

ah so I just need to make roots all the four root parts?

yeah or just put them as parameters. they are only an example.

Alright I got it working but how would I also change the orientation, it doesn’t have to use arc-length parameterization.

Ive tried:

workspace.cart.Orientation = cubic(workspace.p0.Orientation, workspace.p1.Orientation, workspace.p2.Orientation, workspace.p3.Orientation, 1 / i)

But it doesn’t work, and im not sure what to change “1 / i” to (i changed the “_” to “i” if your wondering

why not just do CFrame.lookAt(p0, p1)

What do you mean p0 and p1? Where would I put this?

I don’t know how you’re placing the parts. p0 is the position the part is at, p1 is the position it should be facing ie the next part. this won’t work when it reaches the final position, fyi.

the parts are already there in workspace

I think i’d better off rotating it in another irrelevant script

I don’t know if anyone needs this but here is the end product:

local BezierManager = {}

local function lerp(p0, p1, t)
	return (1 - t) * p0 + t * p1
end

local function quadratic(p0, p1, p2, t)
	local L1 = lerp(p0, p1, t)
	local L2 = lerp(p1, p2, t)
	return lerp(L1, L2, t)
end


local function cubic(p0, p1, p2, p3, t)
	local Q1 = quadratic(p0, p1, p2, t)
	local Q2 = quadratic(p1, p2, p3, t)
	return lerp(Q1, Q2, t)
end

-- PLOTTING --
function bezier3(n, p0, p1, p2, p3)
	local d = 1 / n



	local points = {}
	points[1] = {
		["Position"] = p0, -- starting point
		["Distance"] = 0
	}

	for t = d, 1 + d, d do
		local position = cubic(p0, p1, p2, p3, t) -- get unweighted position
		local previous = points[#points] 
		local gap = (position - previous.Position).Magnitude
		local distance = previous.Distance + gap

		-- store the vector3 as position, total distance as distance
		-- and distance from the previous point as gap
		points[#points+1] = {
			["Position"] = position,
			["Distance"] = distance,
			["Gap"] = gap
		}
	end

	-- total curve length
	local length = points[#points].Distance
	local bezier = {}

	for t = d, 1 + d, d do
		local target = length * t

		-- find the first point that is >= target and the one before it
		-- that means the target length is in between these two points
		local p0, p1
		for i, point in pairs(points) do
			if point.Distance >= target then
				p0 = points[i-1]
				p1 = point
				break
			else
				-- use the final point if target didn't match
				p0 = points[#points-1]
				p1 = points[#points]
			end
		end

		bezier[#bezier+1] = lerp(
			p0.Position,
			p1.Position,
			(target - p0.Distance) / p1.Gap
		)
	end

	return bezier
end

function BezierManager:TravelBezier(p0, p1, p2, p3, numPoints, cart)
	for i, point in pairs(bezier3(numPoints, p0.Position, p1.Position, p2.Position, p3.Position)) do
		cart.Position = point
		local part = Instance.new("Part")
		part.Material = "Neon"
		part.BrickColor = BrickColor.White()
		part.Size = Vector3.new(0.4, 0.4, 0.4)
		part.Position = point
		part.Anchored = true
		part.CanCollide = false
		part.Parent = workspace
		wait(1 / numPoints)
	end
end

return BezierManager

Put this in a ModuleScript under ServerScriptService and title it “BezierModule”

In a script of your choice and you can call the module by doing:

local BezierModule = require(game.ServerScriptService.BezierModule)

BezierModule:TravelBezier(workspace.p0, workspace.p1, workspace.p2, workspace.p3, 100, workspace.cart)

the 4 points can be whatever you want them to just make sure they have a Position Property, the “100” is the number of points you want in your Bezier curve, and workspace.cart is the thing that traverses the Bezier curve. Enjoy!

Uncopylocked game: Bezier Curves - Roblox

2 Likes