Calculate a smoothing tangent between two World-Space Points?

Need a bit of advanced help on this one.

In my game we plot the orbits of objects around a planet using a Spline Component. In order to get a nice smooth curve, we use 64 points. The only purpose of the spline components is to create smooth tangents which I can feed into a Particle system, see below:



// Sets/Updates The Particle In Relation to The Spline Points
void AGESGame_Debris::SetTLEParticleSplinePoints(TArray<FVector> SplinePoints)
{
	TLE_Spline->SetSplineWorldPoints(SplinePoints);

	//Works This Ways And Connects Last To First Point 
	uint32 LastIndex = TLE_Spline->GetNumSplinePoints();
	ASSERTV(LastIndex > 0, *FString::Printf(TEXT("No Spline Points Exist")));

	// Connects The Beam In A Tangent Between 2 Points
	for (uint32 i = 0; i < LastIndex; i++)
	{
		GetTLEParticle()->SetBeamSourcePoint(0, TLE_Spline->GetWorldLocationAtSplinePoint(i), i + 1);
		GetTLEParticle()->SetBeamSourceTangent(0, TLE_Spline->GetWorldTangentAtDistanceAlongSpline(TLE_Spline->GetDistanceAlongSplineAtSplinePoint(i)), i + 1);

		GetTLEParticle()->SetBeamTargetPoint(0, TLE_Spline->GetWorldLocationAtSplinePoint(i + 1), i + 1);
		GetTLEParticle()->SetBeamTargetTangent(0, TLE_Spline->GetWorldTangentAtDistanceAlongSpline(TLE_Spline->GetDistanceAlongSplineAtSplinePoint(i + 1)), i + 1);
	}
}


So, I feed in a bunch of points and then update the beams location and tangents based on that. However, It’s probably better for me to just calculate those tangents manually though math, and then apply them, as it saves me having to use the spline and do a lot of extra function calls to get the smooth tangents.

I’m hoping that this will also allow me to have more control over the tangents, and therefore get a much smoother result. So, has anybody worked with this kind of stuff? The Beam Particle will simply form a full-circle in the end. Thankfully the Beam Particle can do automatic interpolation/sub-beams, so I don’t have to replicate hundreds of points to make it smooth.

As I understand it, getting a direction vector pointing from point A to point B is B-A:

then you might need to normalize it, if it needs to be unit length.

Tho this could give sharp corners, so maybe you can approximate for he series A, B & C by to take from A to B and B to C, averaging those and optionally normalize that result.

Is that what you where looking for?

Maybe debug-rendering those can show if it is close to what you want?

You need 3 points to get a smooth arc. 2 points won’t do. Say you have 4 points a,b,c and d. To find the curve between b and c, you need to find the arc between a,b,c and b,c,d and average. And by arc I mean use the 3 points and finds the circle touching those 3 points.

To find the curve from a to be, you need to find the circle which touches a,b,c. But is there are 4 points like the above example, you need to interpolate between abc arc and bad arc to get b,c curve.

I am replying through mobile so I might not be clear.

@ - Yeah that’d just give me straight lines between each point, what I need is a Bezier tangent of some kind between those points that gives the particle system a tangent, so it curves away rather than snaps to each point.

So I ran a few early tests, I tried to run with four points, but still got pretty harsh edges, so instead I’ve gone for eight points right now, which looks just fine and is certainly acceptable, not worked on the tangent math yet but will certainly get there. I may be able to check out the Spline Component code and go from there.

vect.png
For point B maybe you can use C-A?
Or Normalize(Normalize(B-A)+Normalize(C-B))

[edit]
You will need to check if the length/size of the tangent has any effect on the result.
Scaling it based on Size(C-A) or Max(Size(B-A),Size(C-B)) or Min(Size(B-A),Size(C-B)) could be an idea.
(Like multiplying one of those lengths by a floatConstant or putting it (the length) into some polynomial that maps a float to some scaling factor)

If I use C - A that’ll just give me the direction from A straight to C.

I basically need the ‘Velocity’ at A, but normalized.

In the earlier picture Velocity at B should be rather similar to C-A.

Otherwise; Velocity is the Derivative of the function representing the Position function:

if that is any help.

Thanks I’ll check that out :slight_smile:

Looking through source, found this in InterpCurvePoint.h:



/** Computes Tangent for a curve segment */
template< class T, class U > 
inline void AutoCalcTangent( const T& PrevP, const T& P, const T& NextP, const U& Tension, T& OutTan )
{
	OutTan = (1.f - Tension) * ( (P - PrevP) + (NextP - P) );
}


Looks like that’s the way to do it!

OutTan = (1.f - Tension) * ( (P - PrevP) + (NextP - P) )
Can be simplified
OutTan = (1.f - Tension) * ( P - PrevP + NextP - P )
OutTan = (1.f - Tension) * ( P - P - PrevP + NextP )
OutTan = (1.f - Tension) * ( (P - P) - PrevP + NextP )
OutTan = (1.f - Tension) * ( (0) - PrevP + NextP )
OutTan = (1.f - Tension) * ( - PrevP + NextP )
OutTan = (1.f - Tension) * ( NextP - PrevP )

[edit]
youre back at Normalize(C-A) !

[edit2]
i read Multivariable calculus at a University, BTW

[edit3]
I hope the compiler can optimize really well, if stuff like that is common in the engine, LOL

Ah right, I understand your previous post better now. I though you we’re trying to calculate the Tangent between A and B by doing A - C, which obviously makes no sense.

Perhaps the way Unreal have written it is due to operator precedence with negative numbers or something, I imagine there’s a reason for it.

EDIT: Doesn’t work anyway… ugh

The tangent at B toward C is C-A

Maybe the length should not be unit size?

For a different solution:
For this specific problem where you are moving along a circular path, then:
If you save the center position of the circle and the normal vector for the plane that the circle lies in, then you can calculate the tangent for an arbitrary point by getting the vector between the point you want to calculate it for and the center position and then cross that with the normal

VectorCross(CenterPosition-PositionToGetTangentFor,NormalForCirclePlane)

(you might need to negate the vector depending on if it should face forward/backward)

[edit]
if the center is [0,0,0] and the circle moves in the XY plane then tangent=-pos.y,pos.x,0]

Yeah but that would be in an easy/ideal world :stuck_out_tongue:

Unfortunately it’s not always circular as it’s plotting an orbit, which is a) never ‘perfectly’ circular and can also have some eccentricity. This is how I’m currently attempting to plot the tangents. The problem is, I don’t know if ‘Source Tangent’ in the Particle System is the tangent going into the point, or if it’s the tangent leaving the point. I would guess leaving the point, and ‘Target Tangent’ would be the arrival tangent at the next point.

The code below gives me the same solid lines between each point, like I had no tangents at all. The orbit has to connect at the ends too, hence the special cases for i == 0 and i == last index.



	/* Get amount of entries in the Array, Subtract 1 so we get a full loop */
	uint32 LastIndex = InSplinePoints.Num() - 1;

	for (uint32 i = 0; i <= LastIndex; i++)
	{
		/* If we are the last point, we need to get the first point in the array for the end tangent */
		if (i == LastIndex)
		{
			InputPS->SetBeamSourcePoint(0, InSplinePoints*, i);
			InputPS->SetBeamTargetPoint(0, InSplinePoints[0], i);

			/* Tangent Towards The Next Point In The Spline */
			FVector LeaveTangent = FVector(InSplinePoints[i - 1] - InSplinePoints[0]);
			LeaveTangent.Normalize();

			InputPS->SetBeamSourceTangent(0, LeaveTangent, i);
		}
		else if (i == 0)
		{
			InputPS->SetBeamSourcePoint(0, InSplinePoints*, i);
			InputPS->SetBeamTargetPoint(0, InSplinePoints[i + 1], i);

			/* Tangent Towards The Next Point In The Spline */
			FVector LeaveTangent = FVector(InSplinePoints[LastIndex] - InSplinePoints[i + 1]);
			LeaveTangent.Normalize();

			InputPS->SetBeamSourceTangent(0, LeaveTangent, i);
		}
		else
		{
			InputPS->SetBeamSourcePoint(0, InSplinePoints*, i);
			InputPS->SetBeamTargetPoint(0, InSplinePoints[i + 1], i);

			/* Tangent Towards The Next Point In The Spline */
			FVector LeaveTangent = FVector(InSplinePoints[i - 1] - InSplinePoints[i + 1]);
			LeaveTangent.Normalize();

			InputPS->SetBeamSourceTangent(0, LeaveTangent, i);
		}
	}


Can you test this mod and see what it does:

else
{
InputPS->SetBeamSourcePoint(0, InSplinePoints*, i);
InputPS->SetBeamTargetPoint(0, InSplinePoints[i + 1], i);

/* Tangent Towards The Next Point In The Spline */
FVector LeaveTangent = FVector(InSplinePoints[i - 1] - InSplinePoints[i + 1]);
LeaveTangent.Normalize();

FVector EnterTangent = FVector(InSplinePoints[i - 1+1] - InSplinePoints[i + 1+1]);
EnterTangent.Normalize();

**InputPS->SetBeamSourceTangent(0, -LeaveTangent, i);
InputPS->SetBeamTargetTangent(0, EnterTangent , i);
**}

Nothing Good :stuck_out_tongue: Note: The code you wrote doesn’t change anything, but trying to change the target vectors is the hard part.

Had to modify it a bit so the array didn’t go out of bounds when adding + 2:



		else
		{
			InputPS->SetBeamSourcePoint(0, InSplinePoints*, i);
			InputPS->SetBeamTargetPoint(0, InSplinePoints[i + 1], i);

			/* Tangent Towards The Next Point In The Spline */
			FVector LeaveTangent = FVector(InSplinePoints[i - 1] - InSplinePoints[i + 1]);
			LeaveTangent.Normalize();

			InputPS->SetBeamSourceTangent(0, -LeaveTangent, i);
			DrawDebugDirectionalArrow(InputSC->GetOwner()->GetWorld(), InSplinePoints*, InSplinePoints* + (150 * LeaveTangent), 25.0f, FColor::Yellow, true);

			if (i < LastIndex - 1)
			{
				FVector ArriveTangent = FVector(InSplinePoints* - InSplinePoints[i + 2]);
				ArriveTangent.Normalize();

				InputPS->SetBeamTargetTangent(0, ArriveTangent, i);
				DrawDebugDirectionalArrow(InputSC->GetOwner()->GetWorld(), InSplinePoints[i + 1], InSplinePoints[i + 1] + (150 * ArriveTangent), 25.0f, FColor::Blue, true);
			}
		}


One thing I’ve noticed in the code I wrote in the OP, is that the Target Tangent is exactly the same as the next points Source Tangent.

Hmm, try this:
InputPS->SetBeamSourcePoint(0, InSplinePoints*, i**+1**);
InputPS->SetBeamTargetPoint(0, InSplinePoints[i + 1], i**+1**);

No luck, just shifts the points around and the first two don’t connect :confused:

I WILL figure this out…

Can you make a picture with debug drawing showing what your original code sets the points & tangents to?

Okay, made some progress. Finally got the particles to follow correctly, the issue now is the strength of the Tangents themselves. Below, you can see the current state of the code and the orbit lines. In circular orbits this works pretty well, but for eccentric/elliptical orbits it’s not working so well, I know exactly why, but figuring out the calculation is the hard part.

bf78382c0e2cd4dc4bece8f604a38034f68297fa.jpeg

The above is created using the following code. Note that the ‘GetRolloverArrayIndex’ is just a way to make the array loop back on itself if I give it a number outside of the array bounds, both forwards and backwards. This allows me to join the start and end segments and keep the smoothing.



void UGESGameplayStatics::SetTLEParticleSplinePoints(TArray<FVector>& InSplinePoints, UParticleSystemComponent* InputPS)
{
	/* Get amount of entries in the Array, Subtract 1 so we get a full loop */
	uint32 AmountOfPoints = InSplinePoints.Num();

	/* Calculate the Start & End Point for each segment of the spline, and it's tangent for smooth interpolation */
	for (uint32 i = 0; i <= (AmountOfPoints - 1); i++)
	{
		/* Start Location is always * in the array, Target point is 1 > that */
		InputPS->SetBeamSourcePoint(0, InSplinePoints*, i);
		InputPS->SetBeamTargetPoint(0, InSplinePoints[GetRolloverArrayIndex(AmountOfPoints, i + 1)], i);

		/* Calculate The Tangents. Effectively Source(B) = C - A, Target(B) = D - B. */
		FVector NewSourceTangent = FVector(InSplinePoints[GetRolloverArrayIndex(AmountOfPoints, i + 1)] - InSplinePoints[GetRolloverArrayIndex(AmountOfPoints, i - 1)]);
		FVector NewTargetTangent = FVector(InSplinePoints[GetRolloverArrayIndex(AmountOfPoints, i + 2)] - InSplinePoints*);

		InputPS->SetBeamSourceTangent(0, FVector(NewSourceTangent) / 2.0f, i);
		InputPS->SetBeamTargetTangent(0, FVector(NewTargetTangent) / 2.0f, i);
	}
}

uint32 UGESGameplayStatics::GetRolloverArrayIndex(uint32 InArraySize, uint32 InArrayIndex)
{
	if (InArrayIndex >= 0)
	{
		return InArrayIndex % InArraySize;
	}
	else
	{
		uint32 NewIndex = (InArrayIndex + InArraySize) % InArraySize;
		while (NewIndex < 0)
		{
			NewIndex += InArraySize;
		}
		return NewIndex;
	}
}


The important part is where I divide the tangents by two before feeding them in. Not doing this causes the lines to be fairly straight, but they curve close to the point. Normalizing the tangents is not the right way to go, as we use the size of the tangent as it’s strength (which I didn’t realise until today). Unfortunately they don’t seem to be completely proportional to anything so I don’t get the correct curve on elliptical orbits.

So, this is what I tried instead, which again gets me closer, but still not quite there. All it does it calculate the distance to the next point, and scales the tangent by that amount (after normalizing it).



void UGESGameplayStatics::SetTLEParticleSplinePoints(TArray<FVector>& InSplinePoints, UParticleSystemComponent* InputPS)
{
	/* Get amount of entries in the Array, Subtract 1 so we get a full loop */
	uint32 AmountOfPoints = InSplinePoints.Num();

	/* Calculate the Start & End Point for each segment of the spline, and it's tangent for smooth interpolation */
	for (uint32 i = 0; i <= (AmountOfPoints - 1); i++)
	{
		/* Start Location is always * in the array, Target point is 1 > that */
		InputPS->SetBeamSourcePoint(0, InSplinePoints*, i);
		InputPS->SetBeamTargetPoint(0, InSplinePoints[GetRolloverArrayIndex(AmountOfPoints, i + 1)], i);

		/* Calculate The Tangents. Effectively Source(B) = C - A, Target(B) = D - B. */
		FVector NewSourceTangent = FVector(InSplinePoints[GetRolloverArrayIndex(AmountOfPoints, i + 1)] - InSplinePoints[GetRolloverArrayIndex(AmountOfPoints, i - 1)]);
		FVector NewTargetTangent = FVector(InSplinePoints[GetRolloverArrayIndex(AmountOfPoints, i + 2)] - InSplinePoints*);

		/* Calculate The Distance To The Next Point In The Spline - use this to scale the tangents */
		float TangentStrength = FVector(InSplinePoints* - InSplinePoints[GetRolloverArrayIndex(AmountOfPoints, i + 1)]).Size();

		/* Calculate the strength of the Tangent */
	        NewSourceTangent.Normalize();
		NewTargetTangent.Normalize();

		NewSourceTangent *= TangentStrength;
		NewTargetTangent *= TangentStrength;

		InputPS->SetBeamSourceTangent(0, FVector(NewSourceTangent), i);
		InputPS->SetBeamTargetTangent(0, FVector(NewTargetTangent), i);
	}
}


Again, elliptical orbits seem to suffer the most. Essentially what I have is a situation like the image below, but what I want to do is figure out how strong the tangents should be for each point, and I’ll get my smooth interpolated curve. The image below is a simple plot of Bezier curves using this website: HTML5 Canvas bezierCurveTo generator

I think a contributing factor to why the problem occurs because the distance between the spline points is NOT equal. We divide the orbit into segments based on time, and the further away you are, the longer it takes to go around the Earth so the more dense the points. On the above elliptical orbit, there are only two points close to the Earth, above and below.

So, imagine I have a scenario like below. What I need to do is calculate how long those arms need to be, in order to make the curve completely smooth. I feel like this is something I should be doing a Masters thesis in…

6b18a68a72930eb20c031f45a2497be579a907a5.jpeg

Hi again!

If you check post #7 here:
[

Then you’ll see that my idea for matching a Bezier to a set of points was to have an AI work and try various input and then give you the one that has the smallest error.

Maybe you can do it by pure math, but having an offline AI Tool doing evolutionary trial and error tests might be an acceptable start?