How to get control points of bezier curve from start end and midpoint while retaining continuity?

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

2 Likes

just fyi, you have successfully reverse engineered the implementation of the beam.

since you want to pass through every points, you should make bezier for every pair of points and make them c1 or c2 continuity.

in your example, you should make bezier for points (1,2) (2,3) (3,4) (4,5)


searched this image on google
c1 if CD and QR are same direction
c2 if they are also same magnitude

1 Like

How would I go about doing so? This is similar to what I have been doing before (with three points) with limited success. I am also limited by how beams work. That means I can only do cubic bezier curves which I have been able to do just without continuity.


1 and 3 are inverted this however throws off intersection with midpoint.

2 Likes

you should do bezier on (1,2) and (2,3), not just (1,3)
i’ll expand a little bit. you shall make additional control points
(1, B, C, 2), (2, R, S, 3)
for vector C2 and 2R, we can choose it to be parallel to vector 13 (which is similar to your original choice). 1B is free you can choose whatever direction you like

2 Likes

I have a hard time understanding what you mean by that. I don’t choose how I do the bezier calculations. I reverse am inputing these values to a beam with beams order of operations and everything. I also don’t uderstand what you mean by B,C R, S vecot13? 1B? I am not very familiar with that terminology.

2 Likes

What I was looking for is a way to compensate the points in a way that keeps control-point intersection while having aligned meeting points which I suppose should archieve complete alignment

2 Likes


have you ever used drawing software ? we represent bezier with two end points, and then two vectors (direction, short sticks) to control the curve bending.

here is 5 points like yours. we will have 4 beziers. align the adjacent control sticks directions and magnitude

1 Like

If I understand you correctly, the points are drawn in red and the individual beams are in different colors, defined by only two points (start, end) instead of three (including midpoint). It seems like a doable approach however I have some issues with it.

  1. Determining control points
    I assume the control points would have to be estimated from the surrounding points (only drawing beam between 2 points but using surrounding points for context to estimate curvature)

  2. The beam will be shorter and will only be able to depict simpler curves because of that as it is only using 2 points instead of three.

edit: broken image

1 Like

thank you for color coding it. i have difficulty to do it on phone.

  1. yes correct. you have freedom for all those control stick directions. but you most likely want it to go parallel with its neighbour points as in the figure. not sure how we want for the very start and end point though

  2. each of them is shorter but overall length is roughly the same. because they still have to go through all the points

1 Like

I think I would be able to implement this however that wasnt really what I was looking for. For technical reasons It would complicate a ton of stuff if I had to use the 2 point version instead of the 3 or preferably the 4 point version. I have also seen this done in other peoples projects so it has to be possible. Making the curve reliably pass through the midpoint while having the necessary overlap. I assume this is a lot harder to do than just keeping the adjacent control points in a line.

I remember seeing something similar here: How to approach bullet tracers? (Ex: Phantom Forces bullet tracers) - #25 by rookiecookie153
from that post: bezier refit | Desmos

3 Likes

The problem seems to be even worse with 4 points (rookiecookies math)
https://gyazo.com/b6f6178aff426c929c8951e9621efd89