0 like 0 dislike

I'm trying to get my flocking particle (mesh rendered) to bank as the turn.

I have two problems:

  1. I need to know how to calculate rate of turn.

    Currently I figure that if I store current velocity and last velocity and then do a dot product I'll get a number that I can use to increase or decrease bank.

    As these are birds I'm really only interested in rate of turn from a top down perspective.

    TurningRate = dot(Velocity*float3(1,0,1),LastVelocity*float3(1,0,1));
    LastVelocity = Velocity
  2. I need to take my turning rate and rotate around my forward vector as an axis

    Currently I'm trying to use the rotate() function without much success.

    bankingAngle = normalize(Velocity) * float3(1,0,1);
    bankingAngle = rotate(bankingAngle, normalize(Velocity), deg2rad(180));

‚ÄčI cant get the rotate() function to make any difference at all when using the normalised Vlecoity field as the axis. That means I have no idea if my TurningRate is doing anything useful. Is there a better way to do this?

by feanix (1.2k points)

1 Answer

0 like 0 dislike

Hi Feanix,

first thing, how did you define the orientation of your mesh? you have a ForwardAxisField which is "Velocity", I suppose?

where did you use "bankingAngle"? it's a float3, but which property in the mesh renderer did you use to reference it?

The easiest and most unambiguous way to define the mesh transforms for this kind of things is using a forward and up axis.

Using only a forward, and one of the angular fields, will result in ambiguous orientations that the mesh renderer will try to solve as best as it can (it'll basically create a base pose using the forward axis and the world up axis, and re-orthogonalize it using two cross-products, this will lead to orientations flipping around in some cases, similar to gimbal lock issues)

defining an explicit up axis in addition to the forward axis allows you to lock the transform.
once this base transform is solid, you can add on top other transforms based on angles and things like that.

So, in the example below I'll use the forward + up axis representation.

As usual, there are multiple ways to do this, I'll give two of them.
Both use the mesh renderer setup to use "float3 Velocity" as its "ForwardAxisField" and "float3 Axis" as its "UpAxisField".

1- angle-based instantaneous banking

This will work for coherent horizontal movements, abrupt changes in trajectory will cause an instant snapping of the direction. This needs the second derivative of the motion path to be a continuous function.
The flocking evolver will cause some jittering with this method, but I'll show it anyway as it can have its uses. Driving the motion using the turbulence sampler in 'quintic' interpolation mode will produce a continuous second derivative of the motion path, and the banking will not snap. However, the turbulence sampler has other issues, it'll tend to produce identical values from one frame to the next when running at high framerate, making the velocities identical over a few frames, causing a zero baking, then a frame with different velocities, producing a non-zero banking, this will cause some visual jittering. This typically happens only at high framerates (300+ fps based on the turbulence sampler settings), it should be fine if the sim runs @ 60 fps.

function void Eval()
    float maxBankingSpeed = deg2rad(1.5*360);
    float maxBankingAngle = deg2rad(90);
    float turningRate = saturate((acos(dot(normalize(Velocity), normalize(PrevVelocity)))/dt) / maxBankingSpeed);
    float3 turningAxis = safe_normalize(cross(Velocity, PrevVelocity), 0);

    float bankingAngleSign = sign(dot(scene.axisUp(), turningAxis));
    float bankingAngle = turningRate * maxBankingAngle * bankingAngleSign;

    Axis = rotate(scene.axisUp(), safe_normalize(Velocity), bankingAngle);
    PrevVelocity = Velocity;

Here, we dot the velocity with the previous velocity, which gives us the cosine of the angle between this frame's velocity and the previous frame. dividing by 'dt' gives us the rotation speed in radians per second.
we then divide this by a max turn speed and saturated it to have a value in the 0,1 range that can then be easily used in the following computations.

The axis along which the turn happens is simply the cross product between the velocity and the prev frame's velocity. This will give a zero vector if the particle path does not arc (ie: straight line)

Once we have that, we can use the turning rate to rotate the "up" axis along the forward axis by a value dependent on the turn speed.
Notice the "bankingAngleSign" variable: the initial dot of the two velocities when turning left or right will give the same angle, so to know if we need to bank left or right, we use the sign of the dot between the up axis and the cross product of the two velocities (which will be flipped when turning left compared to when turning right). we then just use that sign to flip the banking angle accordingly.

2- geometric banking, with spring damping.

This is the one that behaves the best in my opinion. As it is damped it shouldn't exhibit jittering even if the motion path's second derivative isn't continuous (even the first derivative for that matter)

Here we don't use angles or trigonometry, just cross products between vectors:

function void Eval()
    float maxBankingSpeed = 0.1;
    float3 turningAxis = cross(Velocity, PrevVelocity)/select(1, dt, dt != 0);
    float3 centrifugalAxis = cross(safe_normalize(Velocity), turningAxis);
    float3 instantBankingAxis = scene.axisUp() + centrifugalAxis * maxBankingSpeed;

    float springStrength = 5.5;
    Axis = lerp(Axis, instantBankingAxis, 1-exp(-dt*springStrength));

    float3 sideAxis = safe_normalize(cross(Velocity, Axis));
    Axis = safe_normalize(cross(sideAxis, Velocity));
    PrevVelocity = Velocity;

The three parts of the script are split as follows:
1- computing the banking axis.
To compute that, we first compute a "centrifugal axis", which is in fact an inverse centrifugal axis, which points opposite to the centrifugal force exerted on the particle, that is, towards the center of the motion path curvature center. The longer it is, the stronger the turn, and the stronger the banking needs to be.
From this vector, we compute a "banking" axis which is basically the new up vector, by adding it to an arbitrary upwards-facing vector. this will give an vector close or equal to the up vector for very large-radius turns (small centrifugal vector), and a vector closer to the pure centrifugal vector for very sharp turns.

2- smoothly make the current up vector go towards that instantaneous banking vector, using a lerp and an 'exp', increase the 'springStrenght' variable value to make the spring harder.

3- re-orthogonalize the up vector with the forward vector, by computing the side axis using a first cross-product between the forward and non-orthogonal up, and recomputing the new orthogonal up vector by using a second cross product between the side and the forward.

Produces this:

Note: if you need a 3D behavior, instead of this:

float3 instantBankingAxis = scene.axisUp() + centrifugalAxis * maxBankingSpeed;

you can try using this, might produce better volumetric banking behaviors:

float3 instantBankingAxis = safe_normalize(turningAxis) + centrifugalAxis * maxBankingSpeed;

Hope this helps !

by Julien (32.8k points)
Did this help?
If you need a more concrete example I can upload the effect file or send it over mail if you drop us a mail.