I have been working on creating a quadratic Bezier curve in Roblox, in 3D, using a bit of a roundabout way. Doing this the normal way is a quite trivial task;
local function CalculateQuadraticBezierCurve(startPos, controlPos, endPos, t)
local u = 1 - t
local tt = t * t
local uu = u * u
local p = uu * startPos
local q = 2 * u * t * controlPos
local r = tt * endPos
local tan = 2 * u * (controlPos - startPos) + 2 * t * (endPos - controlPos)
return p + q + r, tan
end
You can just use this code to set the position of a part directly (p + q + r) and that gives the intended curve.
"Unfortunately", I really want to implement this using FastCast. Some knowledge on FastCast is probably handy for this question, but very quickly: FastCast is a module that creates a raycast every frame based on a initial velocity, direction and acceleration (https://devforum.roblox.com/t/making-a-combat-game-with-ranged-weapons-fastcast-may-be-the-module-for-you/133474). You can only change the direction and velocity initially, but you can change the acceleration on every update which is what I'm doing. This is my current implementation on creating the Bezier curve right now:
On starting the curve:
local startingCFrame = CFrame.new(startPos, endPos)
controlPos[i] = (startingCFrame + (direction * (ClientRange/1.5)) + (startingCFrame:ToWorldSpace(CFrame.Angles(0, 0, math.rad(120 * i + 90))).RightVector * 4)).Position -- The third point on the curve, the control position, is different for each projectile. I fire three here, so 120 degrees from each other
local distance = CalculateQuadraticBezierCurveLength(startPos, controlPos[i], endPos, 100)
totalTime = distance / BULLET_SPEED -- So here we approximate the duration (which turns out to not be correct because of the velocity speeding up beyond the specified BULLET_SPEED)
startingTime[i] = tick()
local passedTime = tick() - startingTime[i]
local alpha = passedTime / totalTime
local _, tan = CalculateQuadraticBezierCurve(startPos, controlPos[i], endPos, alpha)
CastBehavior.Acceleration = tan -- Uses the tangent as acceleration
CastBehavior.Count = i -- As I fire 3 projectiles at once, you want to keep track of which is which
The part that changes the acceleration every frame:
local passedTime = tick() - startingTime[cast.RayInfo.Count]
local alpha = passedTime / totalTime
local pos, tan = CalculateQuadraticBezierCurve(startPos, controlPos[cast.RayInfo.Count], endPos, alpha)
cast.StateInfo.Trajectories[1].Acceleration = tan
An approximation of calculating the curve length:
local function CalculateQuadraticBezierCurveLength(startPos, controlPos, endPos, numSegments)
local length = 0
local previousPoint = startPos
for i = 1, numSegments do
local t1 = (i - 1) / numSegments
local t2 = i / numSegments
local point1, tangent1 = CalculateQuadraticBezierCurve(startPos, controlPos, endPos, t1)
local point2, tangent2 = CalculateQuadraticBezierCurve(startPos, controlPos, endPos, t2)
length = length + (point2 - previousPoint).magnitude
previousPoint = point2
end
return length
end
So what I do is I supply an intended speed and calculate the length of the Bezier curve, and use that to get the alpha on each frame to supply that into the position and tangent calculation. Unfortunately I can only change the acceleration, so based on some googled posts and help from ChatGPT (which wasn't too helpful to be honest), I used that tangent as the acceleration.
Here's a pic for some illustration; the green line is how setting the position works, and is thus a close approximation of the true Bezier curve, and the black line is what happens when using my implementation. Initial problem So naturally, since I input an acceleration the point on the line speeds up over time and breaks the curve. I want to keep a somewhat constant velocity despite the acceleration and follow the actual Bezier curve that I calculate.
So I have the problem of trying to put a Brezier curve in 3D space, and using the slope of the 2D curve to try and model that. So my main idea so far is to get that 2D plane within the 3D space on which the curve moves and correct the acceleration (tangent) for that. The image below shows what I mean visually. 2D plane in 3D
This is the code I add to the code when starting the curve:
local v = (FirePointObject.WorldPosition - TargetPos)
local CFrameLength = CFrame.new(TargetPos + 0.5 * v, FirePointObject.WorldPosition) * CFrame.fromOrientation(0, 0, math.rad(120 * i + 90))
local CFramePerpendicular = CFrameLength * CFrame.Angles(0, 0, math.rad(90))
mod[i] = Vector3.new(math.abs(CFramePerpendicular.UpVector.X), math.abs(CFramePerpendicular.UpVector.Y), math.abs(CFramePerpendicular.UpVector.Z))
It's a bit hard to put into words, but this mod[i] gives (per curve, as I make 3) the proportion of the 3 axes the 2D plane lies on. The correction is simple:
cast.StateInfo.Trajectories[1].Acceleration = tan * mod[cast.RayInfo.Count]
Unfortunately, this does not give proper results; it makes the curve a bit more 'unpredictable', gives different endpoints (so compared to the earlier implentation, the velocity is not equal between curves) and the curve changes depending on with what direction I launch it (which was not the case with the previous implementation). Not a solution #1 Not a solution #2
I am now a bit at a loss, also conceptually but since I'm pretty poor at math also in that department. Maybe I do have the right idea here, but just don't implement it properly. I really hope someone can help me here, or can give some closure if it is just impossible. Using the tangent of a "2D curve" as acceleration (which, again, is mandatory unless I completely change the FastCast module -which right now I can't) is just really weird and more difficult than possible, so I haven't quite found any similar case on the internet. Or I just don't understand any solutions that are on the internet (I'm a somewhat decent guy with luau, but that's honestly not enough at all combined with sucking at math). I see this as a bit of a challenge by now, and honestly I'm also a bit frustrated with myself for not being able to figure it out. So I really really hope someone can at least give some pointers, and I'd be happy to provide context or even the model directly if someone is actually interested to help me out. Thank you in advance!
P.S. I made an account for this and thus a first time asker, hopefully this is properly done.
The velocity is the derivative of the position function, and the acceleration is the derivative of the velocity. Given that you know the original quadratic function formula,
B_x(t) = p_x * (1-t)^2 + 2 * q_x * (1-t)t + r_x * t^2
B_y(t) = p_y * (1-t)^2 + ...
B_z(t) = p_z...
you also know the velocity, because the derivative of a Bezier curve is just a lower order Bezier curve:
B_x(t)' = p_x' * (1-t) + q_x' * t
B_y(t)' = p_y' * (1-t) + q_y' * t
B_z(t)' = p_z' * (1-t) + z_y' * t
where p_x'
is 2 * (q_x - p_x)
, q_x'
is 2 * (r_x - q_x)
, with the same applied to the y and z coordinates.
And similarly the acceleration is the derivative of that, which are just constants because it's the second derivative of a quadratic function.
B_x(t)'' = p_x''
B_y(t)'' = p_y''
B_y(t)'' = p_z''
where p_x''
is q_x' - p_x'
, and the same for the y coordinate.
Conveniently, the t
in the Bezier formula is a time parameter, so you don't even have to do any crazy time reparameterization: you know the position, velocity, and acceleration at every moment in time along the Bezier curve.