I have a list of positions through which I want a beam to pass through. every frame the beam will be moved forward by one step. By testing I have concluded that Beams are drawn as a Cubic bezier curve with unit rightVectors of cframes of attachment 0 and attachment1 multiplied by beams’ CurveSize0 and CurveSize1 being determining Vector3 positions of control points where: A0.WorldPosition = p0 (startPoint), A0.WorldPosition + A0.WorldCFrame.RightVector * beam.BeamSize0 = p1 (control point), A1.WorldPosition + A1.WorldCFrame.RightVector * beam.BeamSize1 = p2 (control point) and finally A1.WorldPosition = p3 (end point).
I have been able to write a function that calculates the p1 and p2 control points using the start, end and mid point (intersecting all of them) I can also specify at which part of the curve should the midpoint be intersected with a time (t) value that goes from zero to one.
function CFrameWithRightVector(position, lookAtTarget)
if position == lookAtTarget then
return CFrame.new(position) -- Return a default CFrame if positions are the same
end
local direction = (lookAtTarget - position).Unit
return CFrame.new(position, position + direction) * CFrame.Angles(math.rad(-90), 0, math.rad(90))
end
local function updateBeam(beam, positionOne, positionTwo, midPoint)
local s = oneWaySine(tick(),1)
local A0 = beam.Attachment0
local A1 = beam.Attachment1
local p0 = positionOne
local p3 = positionTwo
local p1, p2 = SolveBezierControlPointsFixed(p0, p3, midPoint, 0.5)
local p1Magnitude = (p1 - p0).Magnitude
local p2Magnitude = (p2 - p3).Magnitude
--local p1P = makePart("p1".." - "..tostring(beam.Name))
--local p2P = makePart("p2".." - "..tostring(beam.Name))
--p1P.Color = Color3.fromRGB(255, 190, 24)
--p2P.Color = Color3.fromRGB(172, 28, 255)
--p1P.Position = p1
--p2P.Position = p2
A0.WorldCFrame = CFrameWithRightVector(p0, p1)
A1.WorldCFrame = CFrameWithRightVector(p3, p2)
beam.CurveSize0 = p1Magnitude
beam.CurveSize1 = -p2Magnitude
end
function SolveBezierControlPointsFixed(P0, P3, Pm, t)
if t <= 0 or t >= 1 then
error("t must be between 0 and 1 (exclusive)")
end
local oneMinusT = 1 - t
-- Compute the coefficients for the Bezier curve
local A = 3 * oneMinusT^2 * t
local B = 3 * oneMinusT * t^2
local C = oneMinusT^3
local D = t^3
-- Solving the equations to find P1 and P2
-- The system is based on the parametric cubic equation for Bezier curves
local P1 = (Pm - (C * P0 + D * P3)) / (A + B)
local P2 = (Pm - (C * P0 + D * P3)) / (A + B)
return P1, P2
end
This is what that currently looks like with the t values at 0.5
To make the curve move, each frame the updateBeam() function will be called with new points. The points are visualized as the grey cubes. So in frame one the blue cube on the right will be startPoint, The first cube after thata will be midPoint and the one after that will be endPoint. A frame after that what used to be the midPoint becomes the startPoint, endPoint becomes midpoint and a new endPoint is taken from the list. This process repeats until the end of the list is reached. It the picture you can see all the beam configurations at once when in game you would ofcourse only see one at a time. The bezier curve (beam) succesfully intersects all three points, however if I let it run like this it would appear highly wiggly and not smooth at all because as seen in the picture it is not continuous.
To make it continuous the points have to be adjusted so that the ‘tail’ of every curve is aligned with the previous ones front. The result would be one continuous complex curve because they all overlay each other. I suppose it would require storing the last p2 control point and using it to adjust the new p1 and possibly p2 point. It is also possible that manupulating t values could help with this. I have been trying to get this working for two days, but the usual result was a bunch of spaghetti.
Edit: Image shows control points for three curves: 1 in red, 2 blue, 3 purple. For continuity, first control point of 3rd curve should probably be aligned with second control point of curve number 1. This is the desired outcome.
Here is the full code and file
BeamTest.rbxl (58.9 KB)
local function makePart(name)
local part = Instance.new("Part")
part.Size = Vector3.new(1,1,1)
part.Anchored = true
part.CanCollide = false
part.Parent = script.Parent.Parent
if name then
part.Name = name
end
return part
end
local function makeBeam(name)
local beam = script.Beam:Clone()
beam.Parent = workspace.Terrain
local A0 = Instance.new("Attachment")
A0.Name = "A0"
A0.Parent = workspace.Terrain
local A1 = Instance.new("Attachment")
A1.Name = "A1"
A1.Parent = workspace.Terrain
beam.Attachment0 = A0
beam.Attachment1 = A1
beam.Name = name
local randColor = BrickColor.Random().Color
beam.Color = ColorSequence.new(randColor,randColor)
return beam
end
local function count(tbl)
local x = 0
for _,_ in pairs(tbl) do
x+=1
end
return x
end
local toDestroy = {}
local Objects = script.Parent.Parent
local Passes = {}
for _,v in pairs(Objects:GetChildren()) do
Passes[tonumber(v.Name)] = v
end
local amtOfBeams = count(Passes) - 2
local currentBeams = {}
local function CubicBezier(p0, p1, p2, p3, t)
return (1 - t) ^ 3 * p0 + 3 * (1 - t) ^ 2 * t * p1 + 3 * (1 - t) * t ^ 2 * p2 + t ^ 3 * p3
end;
local function oneWaySine(t, speed)
return math.sin(t*speed)/2 + 0.5
end
local t0 = tick()
local function RegenerateBeams()
for i,v in pairs(currentBeams) do
v.Attachment0:Destroy()
v.Attachment1:Destroy()
v:Destroy()
currentBeams[i] = nil
end
for x = 1,amtOfBeams do
local beam = makeBeam(tostring(x).." > "..tostring(x + 2))
currentBeams[x] = beam
end
end
RegenerateBeams()
function SolveBezierControlPointsFixed(P0, P3, Pm, t)
-- Ensure t is within the valid range
if t <= 0 or t >= 1 then
error("t must be between 0 and 1 (exclusive)")
end
local oneMinusT = 1 - t
-- Compute the coefficients for the Bezier curve
local A = 3 * oneMinusT^2 * t
local B = 3 * oneMinusT * t^2
local C = oneMinusT^3
local D = t^3
-- Solving the equations to find P1 and P2
-- The system is based on the parametric cubic equation for Bezier curves
local P1 = (Pm - (C * P0 + D * P3)) / (A + B)
local P2 = (Pm - (C * P0 + D * P3)) / (A + B)
return P1, P2
end
local function epsilon(value)
local epsilon = 1e-7
local t = typeof(value)
if t == "number" then
if math.abs(value) < epsilon then return epsilon, true end
else
return value
end
end
function CFrameWithRightVector(position, lookAtTarget)
if position == lookAtTarget then
return CFrame.new(position) -- Return a default CFrame if positions are the same
end
local direction = (lookAtTarget - position).Unit
return CFrame.new(position, position + direction) * CFrame.Angles(math.rad(-90), 0, math.rad(90))
end
local function updateBeam(beam, positionOne, positionTwo, midPoint)
local s = oneWaySine(tick(),1)
local A0 = beam.Attachment0
local A1 = beam.Attachment1
local p0 = positionOne
local p3 = positionTwo
local p1, p2 = SolveBezierControlPointsFixed(p0, p3, midPoint, 0.5)
local p1Magnitude = (p1 - p0).Magnitude
local p2Magnitude = (p2 - p3).Magnitude
--local p1P = makePart("p1".." - "..tostring(beam.Name))
--local p2P = makePart("p2".." - "..tostring(beam.Name))
--p1P.Color = Color3.fromRGB(255, 190, 24)
--p2P.Color = Color3.fromRGB(172, 28, 255)
--p1P.Position = p1
--p2P.Position = p2
A0.WorldCFrame = CFrameWithRightVector(p0, p1)
A1.WorldCFrame = CFrameWithRightVector(p3, p2)
beam.CurveSize0 = p1Magnitude
beam.CurveSize1 = -p2Magnitude
end
local function update()
--RegenerateBeams()
--prevP2 = Vector3.new(0.1,0,0)
for x = 1,amtOfBeams do
local beam = currentBeams[x]
local positionOne = Passes[x].Position
local positionTwo = Passes[x + 2].Position
local midPoint = Passes[x + 1].Position
updateBeam(beam, positionOne, positionTwo, midPoint)
end
end
update()
while task.wait() do
update()
end
edit: broken image