FastCast and bezier curve; constant velocity problem

Hi everyone,
I’m currently working on using FastCast to create projectives that move along a (quadratic) bezier curve. Potentially helping me out here does require some knowledge of the FastCast module, but the main the thing is that you work under the limitation that you can only modify a projectile’s trajectory by changing the acceleration.

Well, first things first:
The quadratic bezier curve calculation:

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 tangent = 2 * u * (controlPos - startPos) + 2 * t * (endPos - controlPos)

	return p + q + r, tangent
end

And also the calculation for the approximate total length of the curve:

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

These functions are used in the FastCast handler where you activate the tool:

local startingCFrame = CFrame.new(FirePointObject.WorldPosition, TargetPos)
p1[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.
local distance = CalculateQuadraticBezierCurveLength(FirePointObject.WorldPosition, p1[i], TargetPos, 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(FirePointObject.WorldPosition, p1[i], TargetPos, alpha) -- This calculates the tangent
CastBehavior.Acceleration = tan
CastBehavior.Count = i -- As I fire 3 projectiles at once, you want to keep track of which is which

And finally the acceleration gets updated on every ray update:

local passedTime = tick() - startingTime[cast.RayInfo.Count]
local alpha = passedTime / totalTime
local _, tan = CalculateQuadraticBezierCurve(FirePointObject.WorldPosition, p1[cast.RayInfo.Count], TargetPos, alpha)
cast.StateInfo.Trajectories[1].Acceleration = tan

So, this implementation unfortunately speeds up the projectile far beyond the intended total speed, and because of that also screws up the curve:

I tried a couple solutions, but since maths is a bit hard on me generally, most were fruitless anyway. I think that my best idea was to get the 2D planes on which these curves actually move

and correct the calculated tan for that before I put it in the acceleration (very hacky and not performant at all, but it’s in the testing phase still so I want to properly visualise what’s happening):

local line = Instance.new("Part")
line.Anchored = true
line.CanCollide = false
line.CanQuery = false
local v = (FirePointObject.WorldPosition - TargetPos)
line.Size = Vector3.new(0.1, 0.1, v.Magnitude)
line.CFrame = CFrame.new(TargetPos + 0.5 * v, FirePointObject.WorldPosition) * CFrame.fromOrientation(0, 0, math.rad(120 * i + 90))
line.Color = Color3.fromRGB(i * 80, 0, 0)
line.Parent = workspace
local line2 = Instance.new("Part")
line2.Anchored = true
line2.CanCollide = false
line2.CanQuery = false
line2.Size = Vector3.new(0.1, 3, 3)
line2.Position = line.Position
line2.CFrame = line.CFrame * CFrame.Angles(0, 0, math.rad(90))
line2.Color = Color3.fromRGB(i * 80, 0, 0)
line2.Parent = workspace
	
mod[i] = Vector3.new(math.abs(line2.CFrame.UpVector.X), math.abs(line2.CFrame.UpVector.Y), math.abs(line2.CFrame.UpVector.Z))

And then:

local passedTime = tick() - startingTime[cast.RayInfo.Count]
local alpha = passedTime / totalTime
local _, tan = CalculateQuadraticBezierCurve(FirePointObject.WorldPosition, p1[cast.RayInfo.Count], TargetPos, alpha)
cast.StateInfo.Trajectories[1].Acceleration = tan * mod[cast.RayInfo.Count]

This changes the trajectories the projectiles follow depending on the axes it is launched and only slightly decreases the speed increase; so not a solution. Maybe my implementation is way too shabby though, so I haven’t quite given up on the idea.

I hope someone can delve into this challenge and help me out. Conceptually helpig would be great as well. By now it’s just quite frustrating hahaha. Not using FastCast makes the whole thing pretty much trivial, see this example I made:
Quadratic Bezier Curve.rbxm (6.2 KB)
So that example is the result I want using FastCast. Thanks in advance!

2 Likes

Here’s maybe better screens of what is happening (the black arrows) and what I would want to happen (green):


1 Like

A little push, maybe there’s someone able to help :smiley:

1 Like

Does it really expect acceleration? The tangent to the curve is the first derivative of the position, i.e. the velocity. Your CalculateQuadraticBezierCurve is not even computing the second derivative, which would be the acceleration, and would look like this:

local d2f = 2 * ( endPos - 2 * controlPos + startPos )

The other problem is that your bezier is not an arc-length parameterization, so it’s not going to give you constant-speed sample points, the samples will be closer together (with slower velocity tangents) where curvature is higher, which is not what you want.

Really, you want to do an arc-length parameterized resampling of the bezier curve if what you really want is constant speed of the projectile. Pretty sure there is still a tutorial covering this in the tutorial section of this site, so I won’t derive it here. Once you do this, the first difference between sample points can be used as velocity, and the first difference between velocities is an approximation of acceleration. When you verify that you have constant-speed sample points (evenly spaced) then you can just use the direction of the tangent for velocity and rescale it for any projectile speed too.

That said, the results you got suggest to be that the FastCast “Acceleration” might actually be a velocity. Your curves are about the right shape, which they would not be if you fed in the 2nd derivative of your curve as acceleration.

Lastly, your CalculateQuadraticBezierCurveLength function calls CalculateQuadraticBezierCurve about twice as many times as it needs to, since t1 for a given iteration of the loop is the t2 from the previous iteration, so just save previousTangent in addition to previousPoint, and make your loop have just a single t value (what you currently call t2 is the only one you need).

2 Likes

Thank you for the response!

Does it really expect acceleration? The tangent to the curve is the first derivative of the position, i.e. the velocity.

That said, the results you got suggest to be that the FastCast “Acceleration” might actually be a velocity. Your curves are about the right shape, which they would not be if you fed in the 2nd derivative of your curve as acceleration.

Yeah, it does expect acceleration. Unfortunately, Bezier curves have a constant acceleration (as you give). For a point to move as a curve in 3D, without any other forces acting on it, a constant acceleration doesn’t work. So I was hoping for a different approach.

Really, you want to do an arc-length parameterized resampling of the bezier curve if what you really want is constant speed of the projectile. Pretty sure there is still a tutorial covering this in the tutorial section of this site, so I won’t derive it here.

Once you do this, the first difference between sample points can be used as velocity, and the first difference between velocities is an approximation of acceleration. When you verify that you have constant-speed sample points (evenly spaced) then you can just use the direction of the tangent for velocity and rescale it for any projectile speed too.

This is what I’ve tried to do since as well, I haven’t quite figured it out (or it just doesn’t work I guess). Just dividing the difference in positions (forgot the 3D term for it, sorry) by the delta to first get the velocity and then the acceleration gives enormous accelerations. Dividing by it once, to get the velocity is indeed just the tangent as we’ve established. Maybe the speed being way to high at the start just screws it up, so I’ll at least try again.

Lastly, your CalculateQuadraticBezierCurveLength function calls CalculateQuadraticBezierCurve about twice as many times as it needs to, since t1 for a given iteration of the loop is the t2 from the previous iteration, so just save previousTangent in addition to previousPoint, and make your loop have just a single t value (what you currently call t2 is the only one you need).

Thank you! I’ll also implement this. A simple thing, but if you don’t think of it, it’s unfortunate.