Catmull-Rom Spline Class (Superior to Bezier Curve)

Demonstration

Catmull-Rom Spline Class

--[[
	Catmull-Rom Spline Class
	
	This class takes n number of control points using Vector3s.

	--]]

local CatmullRomSpline = {}
CatmullRomSpline.__index = CatmullRomSpline

function CatmullRomSpline.new(points)
	local self = setmetatable({}, CatmullRomSpline)
	self.points = points
	return self
end

function CatmullRomSpline:getPoint(t)
	local points = self.points
	local n = #points

	if n < 2 then
		return nil
	end

	if n == 2 then
		return points[1]:lerp(points[2], t)
	end

	local p0 = points[math.max(1, math.floor(t))]
	local p1 = points[math.min(n, math.floor(t) + 1)]
	local p2 = points[math.min(n, math.floor(t) + 2)]
	local p3 = points[math.min(n, math.floor(t) + 3)]

	local t = t - math.floor(t)

	local a = 2 * p1
	local b = p2 - p0
	local c = 2 * p0 - 5 * p1 + 4 * p2 - p3
	local d = -p0 + 3 * p1 - 3 * p2 + p3

	local point = 0.5 * (a + (b * t) + (c * t * t) + (d * t * t * t))

	return point
end

return CatmullRomSpline

Example (Press R to generate new path)

Just add to ServerScriptService or Workspace

llocal Lighting = game:GetService("Lighting")
local UserInputService = game:GetService("UserInputService")

local Terrain = workspace.Terrain

--[[
	Catmull-Rom Spline Class
	
	This class takes n number of control points using Vector3s.

	--]]

local CatmullRomSpline = {}
CatmullRomSpline.__index = CatmullRomSpline

function CatmullRomSpline.new(points)
	local self = setmetatable({}, CatmullRomSpline)
	self.points = points
	return self
end

function CatmullRomSpline:getPoint(t)
	local points = self.points
	local n = #points

	if n < 2 then
		return nil
	end

	if n == 2 then
		return points[1]:lerp(points[2], t)
	end

	local p0 = points[math.max(1, math.floor(t))]
	local p1 = points[math.min(n, math.floor(t) + 1)]
	local p2 = points[math.min(n, math.floor(t) + 2)]
	local p3 = points[math.min(n, math.floor(t) + 3)]

	local t = t - math.floor(t)

	local a = 2 * p1
	local b = p2 - p0
	local c = 2 * p0 - 5 * p1 + 4 * p2 - p3
	local d = -p0 + 3 * p1 - 3 * p2 + p3

	local point = 0.5 * (a + (b * t) + (c * t * t) + (d * t * t * t))

	return point
end

local function GetArrayOfRandomVector3s(n, Factor)
	local ArrayOfRandomVector3s = {}
	local RNG = Random.new()

	for Index = 1, n do
		ArrayOfRandomVector3s[Index] = Vector3.new(
			RNG:NextNumber(-1, 1),
			RNG:NextNumber(-1, 1),
			Index
		) * Factor
	end

	return ArrayOfRandomVector3s
end

local function DrawPoint(Position, Color, Size)

	local Part = Instance.new("Part")
	Part.Color = Color
	Part.Material = Enum.Material.Neon
	Part.Parent = Terrain
	Part.Size = Size
	Part.Position = Position
	Part.CanCollide = false
	Part.Anchored = true
	Part.Shape = Enum.PartType.Ball

	return
end

local function DrawLine(Position1, Position2)
	local Delta = Position2 - Position1
	local Line = Instance.new("Part")
	Line.Color = Color3.fromRGB(255, 1/2*255, 0)
	Line.Material = Enum.Material.Neon
	Line.Parent = Terrain
	Line.Size = Vector3.new(Delta.Magnitude, 1 / 4, 1 / 4)
	Line.CFrame = CFrame.lookAt(Position1 + 1 / 2 * Delta, Position1) * CFrame.Angles(0, math.rad(-90), 0)
	Line.CanCollide = false
	Line.Anchored = true
	Line.Shape = Enum.PartType.Cylinder
end

local n = 10
local Factor = 25
local points = nil
local newCatmullRomSpline = CatmullRomSpline.new({})
local resolution = 1 / 144

local function Generate()
	points = GetArrayOfRandomVector3s(n, Factor)
	newCatmullRomSpline.points = points
	
	for Index = 1, n do
		DrawPoint(points[Index], Color3.fromRGB(1/2*255, 0, 255), Vector3.one)
	end
	
	for Index = 0, n - 1, resolution do
		DrawLine(newCatmullRomSpline:getPoint(Index), newCatmullRomSpline:getPoint(Index + resolution))
	end
	
	return
end

local function Delete()
	Terrain:ClearAllChildren()
	points = nil
	newCatmullRomSpline.points = {}
	return
end

local function OnInputBegan(Input, gameProcessedEvent)
	if Input.KeyCode == Enum.KeyCode.R and not gameProcessedEvent then
		Delete()
		Generate()
	end
end
UserInputService.InputBegan:Connect(OnInputBegan)

local function ChangeEverything()
	for Index, Descendant in ipairs(workspace:GetDescendants()) do
		if Descendant:IsA("BasePart") then
			pcall(Descendant.Destroy, Descendant)
		end
	end
	
	Lighting:ClearAllChildren()
	
	Lighting.Ambient = Color3.fromRGB()
	Lighting.Brightness = 0
	Lighting.ColorShift_Bottom = Color3.fromRGB()
	Lighting.ColorShift_Top = Color3.fromRGB()
	Lighting.EnvironmentDiffuseScale = 0
	Lighting.EnvironmentSpecularScale = 0
	Lighting.GlobalShadows = false
	Lighting.OutdoorAmbient = Color3.fromRGB()
	Lighting.ShadowSoftness = 0
	Lighting.ClockTime = 0
	Lighting.ExposureCompensation = 0
	Lighting.FogColor = Color3.fromRGB()
	Lighting.FogEnd = math.huge
	Lighting.FogStart = -math.huge
end

ChangeEverything()
Delete()
Generate()
24 Likes

Looks like pretty good stuff and might be useful for math games but I don’t need this.

I made it because Bezier Curves always annoyed me, because they don’t pass through the control points. So I looked into splines and made this, if you need to join up a set of points with a curve, use this.

6 Likes

Could you put a few videos of you demonstrating it in the post?

Why? I have already give a coded example.

No, I meant to ask for a few demonstration videos.

I’m still confused why do you want demonstration videos?

The reason is rather obvious; Before people use your code, they usually want to see a video of you demonstrating it.

That seems like a lot of effort, I already put a picture up.

I can barely see what’s in the picture due to the fact you’ve used bright colours in a bright background.

people want demonstration videos for proof, and an example of how it works without touching Roblox Studio, and your image provided provides possibly limited detailed explanation.

Demonstration

1 Like

Thats smooth as hell! thanks for contributing

Just one thing, how optimized is this

The module is as optimized as it can be.

1 Like

Thank you for this module, it’s made my life a lot easier w/a project I’m working on! :smiley:

Do you have any input on how I can go about normalizing the points along the path w/arc-length parameterization) (Bézier Curves)?

Right now it works great, but when it goes to a tight curve from point A to B, it condenses the points down and my tweening logic makes it slow down in those areas and then rapidly speeds up in others :frowning_face:

Do you mean something like this?

--[[
	Catmull-Rom Spline Class
	
	This class takes n number of control points using Vector3s.

--]]

local CatmullRomSpline = {}
CatmullRomSpline.__index = CatmullRomSpline

function CatmullRomSpline.new(points)
	local self = setmetatable({}, CatmullRomSpline)
	self.points = points
	return self
end

function CatmullRomSpline:getPoint(t)
	local points = self.points
	local n = #points

	if n < 2 then
		return nil
	end

	if n == 2 then
		return points[1]:lerp(points[2], t)
	end

	local p0 = points[math.max(1, math.floor(t))]
	local p1 = points[math.min(n, math.floor(t) + 1)]
	local p2 = points[math.min(n, math.floor(t) + 2)]
	local p3 = points[math.min(n, math.floor(t) + 3)]

	local t = t - math.floor(t)

	local a = 2 * p1
	local b = p2 - p0
	local c = 2 * p0 - 5 * p1 + 4 * p2 - p3
	local d = -p0 + 3 * p1 - 3 * p2 + p3

	local point = 0.5 * (a + (b * t) + (c * t * t) + (d * t * t * t))

	return point
end

function CatmullRomSpline:normalizePoints()
	local points = self.points
	local n = #points

	if n < 2 then
		return nil
	end

	local arcLength = self:calculateArcLength()
	local pointSpacing = arcLength / (n - 1)

	for i, point in ipairs(points) do
		point = point * pointSpacing
	end

	return points
end

-- Calculate the arc length of the curve from point A to point B

function CatmullRomSpline:calculateArcLength()
	local points = self.points
	local n = #points
	local arcLength = 0

	for i = 1, n - 1 do
		local pointA = points[i]
		local pointB = points[i + 1]
		local distance = (pointB - pointA).magnitude
		arcLength = arcLength + distance
	end

	return arcLength
end

local function GetArrayOfRandomVector3s(n, Factor)
	local ArrayOfRandomVector3s = {}
	local RNG = Random.new()

	for Index = 1, n do
		ArrayOfRandomVector3s[Index] = Vector3.new(
			RNG:NextNumber(-1, 1),
			RNG:NextNumber(-1, 1),
			Index
		) * Factor
	end

	return ArrayOfRandomVector3s
end

local function DrawPoint(Position, Color, Size)

	local Part = Instance.new("Part")
	Part.Color = Color
	Part.Material = Enum.Material.Neon
	Part.Parent = workspace
	Part.Size = Size
	Part.Position = Position
	Part.CanCollide = false
	Part.Anchored = true
	Part.Shape = Enum.PartType.Ball

	return Part
end

local n = 10
local Factor = 25
local points = nil
local newCatmullRomSpline = CatmullRomSpline.new({})
local resolution = 1 / 10

local function Generate()
	points = GetArrayOfRandomVector3s(n, Factor)
	newCatmullRomSpline.points = points
	newCatmullRomSpline:normalizePoints()

	for Index = 1, n do
		DrawPoint(points[Index], Color3.fromRGB(1/2*255, 0, 255), Vector3.one)
	end
	
	for Index = 0, n - 1, resolution do
		local Point = DrawPoint(newCatmullRomSpline:getPoint(Index, true), Color3.fromRGB(255, 1/2*255, 0), Vector3.one * 1 / 4)
	end

	return
end

Generate()
1 Like

This was the solution I needed, thank you so much for taking the time to assist w/this!

I am curious what exactly are you using my module for?

I wrote an event for Ultimate Driving that has a cargo plane fly around the map dropping off crates that contain ‘gears’ (A currency for the event).

I used your module for calculating the waypoints for me to use w/the plane to make it realistic w/movement (Following curves gently up/down/side-to-side), and also for creating the paths for crates to realistically fall down to.

1 Like