--[[
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()
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.
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.
Thank you for this module, it’s made my life a lot easier w/a project I’m working on!
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
--[[
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()
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.