Curve generation with desired orientation and position

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!
    I want to make a script that can generate a simple bezier curve path from between 2 nodes which also takes into account the direction and orientation of each node.

  2. What is the issue? Include screenshots / videos if possible!
    This is an image of a pre made path that I placed down (not the orientation indicators and their direction)

My current code creates a bezier curve path which doesnt take into account the orientation indicators shown in the first image.

As well, if you look closely at the individual parts, there are gaps between them due to incorrect sizing of the lengths of each part.

  1. What solutions have you tried so far? Did you look for solutions on the Developer Hub?

For the first issue regarding taking into account the orientation, I haven’t tried anything and I’ve been looking for other solution on the developer hub but no such luck :confused:

For the second issue regarding the gaps, I tried to create a script that calculates the distance between two outer edges (the gap between two individual parts) and then calculates the desired size to scale them, but that didn’t work and the sizing was incorrect.

After that, you should include more details if you have any. Try to make your topic as descriptive as possible, so that it’s easier for people to help you!

here is the code for the bezier curve path main script

local interpolation = require(script.Interpolation)

local curve = workspace.curve
local points = workspace.points

local startP
local midP
local endP

local last

local function CalculateProperties(part, pos1, pos2)
	part.Position = pos2 + Vector3.new(0,0,0)
	
	local diff = (pos2 - pos1)
	local angleY = math.deg(math.atan2(diff.X, diff.Z))
	
	part.Size = Vector3.new(part.Size.X, part.Size.Y, diff.Magnitude)
	part.Orientation = Vector3.new(0, angleY, 0)
	part.Parent = curve
end

task.wait(5)

while true do
	curve:ClearAllChildren()
	last = nil
	startP = points.node1.Position
	midP = points.mid.Position
	endP = points.node2.Position
	
	for t = 0, 1, 0.01 do
		local newPos = interpolation.QuadBezier(startP, midP, endP, t)
		local part = game.ReplicatedStorage.part:Clone()
		part.CanCollide = false
		part.Anchored = true
		part.TopSurface = Enum.SurfaceType.SmoothNoOutlines

		if last then
			CalculateProperties(part, last, newPos)
		end

		last = newPos
		
		task.wait()
	end
	
	task.wait(3)
end

here is the interpolation module.

local interpolation = {}

function interpolation.Lerp(a, b, t)
	return (1 - t) * a + t * b
end

function interpolation.QuadBezier(a, b, c, t)
	return (1 - t)^2*a + 2*(1 - t)*t*b + t^2*c
end

function interpolation.CubicBezier(a, b, c, d, t)
	
end

return interpolation

From what I know about bezier curves this is just not possible, a bezier curve is a lerp between x amount of points.

You draw a line from A to B.
And B to A.

You put a point on both lines and lerp them between their respective points by T
and then put a line between them lines and point it between T and there you go. Quad Bezier AFAIK?

Obviously you repeat this for all points you want so you can create any type of bezier curve from any given points.

But the point is this wil go towards B then curve towards C.
I don’t tnik there is a plausible way to make the curve generate like that or I may just be dumb, that’s my give or take.

Oh and your only using position if that helps.

Give me a moment to come up with my own Bezier shenanigans with a CFrame approach, cause again your are only using position.
CFrame is both pos and rot.

Well, my code appears to do it for the end but oops.


The start isn’t quite right.

Bezier Module:

local module = {}

function lerp(a, b, t)
	return a + (b-a) * t --// no difference between our lerp functions/
end

function lerpCF(a, b, t)
	return a:Lerp(b, t) --// roblox has native support.
end

module.bezierFromPoints = function(points, T)
	local curList = points --// just a list of point CFs preferably
	repeat
		local newPos = {}
		for i, p in pairs(curList) do
			if curList[i+1] then
				table.insert(newPos, lerpCF(p, curList[i+1], T))
			end
		end
		curList = newPos
	until #curList == 1	
	return curList[1]
end

return module

Main Script

local bezierModule = require(script:WaitForChild("Bezier"))
local points = {}
local sortedPoints = workspace.BezierPoints:GetChildren()
table.sort(sortedPoints, function(a, b)
	return tonumber(string.sub(a.Name, #"P1")) < tonumber(string.sub(b.Name, #"P1"))
end)

for i, v in pairs(sortedPoints) do
	table.insert(points, v.CFrame)
end

local inc = 0.1

function createPart(a, b)
	local diff = (a.Position - b.Position)
	local newPart = Instance.new("Part", workspace)
	newPart.Size = Vector3.new(3, 1, diff.Magnitude)
	newPart.Anchored = true
	newPart.BrickColor = BrickColor.new("Black")
	newPart.CFrame = CFrame.new(a.Position, b.Position) * CFrame.new(0, 0, -diff.Magnitude/2)
	
	return newPart
end

for i = 0, 1-inc, inc do
	local curBez = bezierModule.bezierFromPoints(points, i)
	local nextBez = bezierModule.bezierFromPoints(points, i+inc)
	createPart(curBez, nextBez)
end

If this gets you anywhere.
As for the sizing issue I’m not quite sure.

Not sure if I did anything wrong but, I don’t think the code worked?

All I did was copy and paste the code, no changes made

I unfortunately said I don’t know myself, I assumed CFrame would do it but then again I’m not the best at bezier curves.

That’s fine, don’t worry! I’m not good at them either and I’m trying to learn so i can make a plugin for this but thank you for the help anyways!

After coming back to this, I realized it was to just simply switch to cubic beziers :slight_smile:

The individual control points make it much easier to create the desired curve given each control point controls it’s respective node.

As for figuring out orientations, I just had to copy the code for calculating angle Y:

angleY = math.deg(math.atan2(diff.X, diff.Z))

and apply that as well to angle X:

angleX = math.deg(math.atan2(diff.Y, diff.Z))

for angle Z however, I noticed it would be more accurate to interpolate the angle with the same formula:

angleZ = interpolation.CubicBezier(a.Orientation.Z, b.Orientation.Z, c.Orientation.Z, d.Orientation.Z, t)

For some reason this results in the generated part facing upside down, so also make sure to add 180 degrees to flip it rightside up.

part.Orientation = Vector3.new(angleX, angleY, angleZ + 180)

This gave me the solution of what I wanted from my bezier curve and hopefully anybody else who wants something like this can use this as well!

I will also take a look at your original code using quadratic beziers and will see how I can implement it with cubic beziers. I will update this post if that result is better than this one.

I was looking at this code and it worked flawlessly with cubic beziers! (thanks :D)

However, for some reason the CFrame lerping function doesn’t work with the orientations because they dont match the rotations.

my original function worked fine with the solution I implemented.

but… I can’t say the same for your function.

It (obviously) has to do with the fact that the rotation values don’t match the orientation values which means the Yaw, Pitch, and Roll of the part aren’t being lerped properly. I know about x, y, z = CFrame:ToOrientation() but im not sure how to involve it with the function.

any suggestions?

What you’re after is more than just a Bezier curve, you want orientation CFrames along the curve, of which there are many solutions. The most common is the Frenet-Serret frame, which is just constructed from the tangent (direction along the curve), normal (direction of curvature), and bi-tangent (cross product of the tangent and normal). But, this frame system has 180 degree discontinuities when curvature changes direction, and is rarely the frame type you want in a game system (e.g. a road or roller coaster track), normally you want a minimal twist frame, or an “upright” frame (trying to preserve the up vector as world up), etc. You can start here: Frenet–Serret formulas - Wikipedia but you’ll probably want to do some thorough Googling of spline orientation frame options.

My goal is to make a railroad track making plugin so would I have to research this extensively to find what I’m looking for?

Ah, well, railroad tracks are a special case that the most common 3 general types of fitting frames to splines doesn’t cover, for a couple of reasons.

First, the actual shape of railroad tracks, as built, cannot be modeled using cubic or quadratic Bezier curves. As seen from the top down–not considering grades and banking yet–railroad tracks are typically built from 3 types of sections: Straight, circular arc, and transition sections. The transition sections are Euler Spiral sections (aka clothoids or Cornu Spirals) that join straight to circular sections smoothly, by continuously changing the radius of curvature from nearly straight down to the radius of the circular arc. The purpose for this is to achieve G2 continuity over the entire spline, so that there are no discontinuities in lateral acceleration (no lateral ‘jerk’) that could cause passengers or loads to fall over or slide around.

Neither circles nor Euler spirals can be implemented perfectly with Bezier curves, but their formulas are simple enough to fit them to key points when making building tools.

As for the full orientation… the desired “normal” for the track, which you would call the banking, is based on the optimal speed of the train through the curve. Track is banked so that centripetal acceleration at the target speed is perpendicular to the “floor” of the tilted train, so that again, there is no lateral acceleration in the frame of the train that could send the load flying off the side or cause passengers to have to lean to compensate. You work this out from the physics, not from basic vector maths like for simpler frame types.

Lastly, there is grade. This is just the elevation change of the track, and is designed specific to the physics of the train (friction of wheels, horsepower, number of cars, etc…). Generally, the more gradual, the better (as you already know). Grade doesn’t usually factor into the orientation of the curve, since it’s normally gradual and insignificant for common trains. Roller coasters have to deal with it, trains not so much.

FWIW, this is all just for your information. You can obviously make a train game that uses bezier or cubic splines to approximate the real shapes. But if you were to make a fully physics-simulated train, doing it the way it’s done in the real world is probably going to give the most natural and expected results, and IMO, it’s not appreciably more difficult to implement, just different. Well, if your train is TGV or Shinkansen speed, you may actually need true G2 continuity and physically-correct banking to keep it on the track XD.

Forgive my lack of knowledge with some of the mathematical terminology you use as I’m trying my best to understand it.

To better clarify what I’m trying to achieve with my plugin, I want to make a system where the user is able to generate a section of railroad track between two nodes by interpolating a curve between them and generating the tracks from that curve.

I also want to grant the user the ability to influence the shape of the generated curve by manipulating control points (like a bezier curve or other curve that uses control points) to achieve the desired shape of the curve.

I didn’t Intend on having the system just automatically make the best path as:

  1. The purpose is to offer the user complete control of the curve.
  2. I think it would take too much time to implement both this system and a manual control system.

The same two principles also apply to issue of Grades and Track shape types (Straight, Circular and Transitional).

I’m also taking some inspiration on this plugin from a video I saw on twitter about a similar plugin which I have been trying to reverse engineer, the type of path generation that it uses:
https://x.com/BuilderAtWork_/status/1435619434937323526

I’d also like to ask: If you watched the first ~20 seconds of the video, there’s a “tension” value that the demonstrator changes the value of multiple times wich affects the… for lack of a better word… tightness of the curve. (higher value → shallower curve). Do you know if this has anything to do with weighted bezier curves or is this something that you think is unrelated?

My apologies for not fully clarifying myself and thanks in advance!