Brendan Keesing   

FOMOGRAPHY     Projects     Blog     About


Complex Procedural Driver Animation

June 19, 2021

While working on Torque Drift, I was given an opportunity to work on a procedural hand animation solution for the driver. This is a seemingly-ambitious proposition, as drift drivers have lots of crazy hand movements, yet can be achieved by using only basic techniques.

HSV

One thing to point out is that this solution is reactive to the player’s input. In other words, the animation will lag behind the player’s actual button presses and will therefore not be 100% accurate. Because there’s so much happening in the cockpit and the player is not reacting to the animation, this should not be a significant issue.

Although this is using the example of driving, the techniques demonstrated here can be easily extended to very different procedural animation problems.

Breaking Down the Problem

To start with, we need a basic rigged character, and a way to perform IK. All of the IK I use are basic 3-point IK solved through trigonometry:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void Solve3DIK(Transform bone1, Transform bone2, Transform bone3, Vector3 targetPosition, Vector3 bendDir)
{
	Vector3 dir = targetPosition - bone1.position;

	// Distance between the first and the last node solver positions
	float length = dir.magnitude;
	if (Mathf.Approximately(length, 0))
		return;

	// Get the direction to the trigonometrically solved position of the second node
	float sqrMag1 = (bone2.position - bone1.position).sqrMagnitude;
	float sqrMag2 = (bone3.position - bone2.position).sqrMagnitude;
	Vector3 toBendPoint = GetDirectionToBendPoint(dir, length, bendDir, sqrMag1, sqrMag2);

	// Position the second node
	Quaternion q1 = Quaternion.FromToRotation(bone2.position - bone1.position, toBendPoint);
	bone1.rotation = q1 * bone1.rotation;

	Quaternion q2 = Quaternion.FromToRotation(bone3.position - bone2.position, targetPosition - bone2.position);
	bone2.rotation = q2 * bone2.rotation;
}

// Calculates the bend direction based on the law of cosines. Magnitude of the returned vector does not equal the length of the first bone.
Vector3 GetDirectionToBendPoint(Vector3 direction, float directionMag, Vector3 bendDirection, float sqrMag1, float sqrMag2)
{
    float x = ((directionMag * directionMag) + (sqrMag1 - sqrMag2)) / (directionMag * 2);
    float y = Mathf.Sqrt(Mathf.Max(sqrMag1 - x * x, 0));

    if (direction == Vector3.zero)
        return Vector3.zero;
    return Quaternion.LookRotation(direction, bendDirection) * new Vector3(0, y, x);
}

This IK will be applied on each hand, forearm and upper arm bone set. This will let us place the hand wherever we want. The elbow bend directions will point down and away from the body, and will remain static for our needs.

All that remains is to figure out where to put the hands. This is the tricky part.

Steering Wheel

Some observations about how the real world drivers use the steering wheel:

  • The hands stay on the steering wheel until their hands are twisting too much.
  • If a hand is twisting too much, often one hand will come off while the other stabilizes the wheel.
  • If a hard turn needs to be made, both hands are removed from the wheel while it spins.
  • After a hand has left the wheel, it will often return to a natural resting point (toward the top of the wheel).
  • One hand must occasionally be used to change gears or pull on the handbrake.
  • Hands will never land on each other and don’t get in the other’s way.

To get the obvious out of the way, the steering wheel rotates on a single axis. Because of this, the hands will naturally move along the edge of a circle. We will need to define this circle in 3D space.

1
2
3
4
5
6
7
class SteeringWheel
{
	Vector3 origin;
	Vector3 normal;
	Vector3 tangent; // points toward upward direction of wheel
	float radius;
}

We can also represent a hand as a scalar angle value where 0 is the very top of the wheel, 90° is to the right, -90° is to the left, and ±180° is the bottom of the wheel. With this, we can assign natural rest points for the hands as something like -30° for the left hand and +30° for the right hand.

HSV

The next vital piece of the puzzle is coordinate spaces; where are these angles relative to. We said earlier that the 0° is the top of the wheel, but is that the top of the wheel at the wheel’s resting point (i.e. when the car is going straight), or is it the top of the wheel relative to the upward direction of the car? Both answers are subjectively correct and have their own benefits. For example, when the hand clamps down on the wheel, we want it to follow the steering wheel, so the position of the hand must be stored in wheel-space. However, when we want to pick a place for the hand to land on the wheel, we don’t want it to check wheel space because the wheel may have rotated more than 180°. Instead, we want to choose a placement in car-space. Due to this, we need a way to convert between car-space and wheel-space, which is really simple if we have the angle that the wheel is currently at.

1
2
3
4
5
6
7
8
9
void ToCarSpace(float angle)
{
	return wheelAngle - angle;
}

void ToWheelSpace(float angle)
{
	return wheelAngle + angle;
}

HSV

So how can we use all this? This is roughly how the algorithm works:

  1. Set wheelAngle to zero, so the wheelAngle = carAngle.
  2. Place the hands at natural rest angles.
  3. As wheelAngle rotates (due to steering) the hands will keep their placement angle in wheel-space (so that they rotate with the wheel).
  4. When the wheel rotates so much that the hand is no longer in a comfortable position (<-70° or >70°), the hand will unclamp it’s hand by converting its placement from wheel-space to car-space.
  5. The hand will then move toward the natural resting point until the steering wheel velocity changes enough to justify the hand clamping down.
  6. The hand will then convert its position from car-space to wheel-space and will follow the wheel once again.
  7. Return to step #3.

There is still one annoying issue: the hands get in the way of each other.

The first thing we can do is to move the hand away from the wheel. We can just move it in the direction of the wheel’s normal a bit should suffice.

HSV

The other thing we can do is ensure that a hand will not land on the other. For this, we can set a buffer area around the hand. Using the left hand as an example, it’s fine for the right hand to be close to the right side, but would be weird to be close to the left. So we will make it that the right hand will not clamp down until the left hand is not in the area underneath.

HSV

Handbrake

When the player hits the handbrake, we need the nearest hand to come off the wheel and move to the handbrake. We can break it down into the following steps:

  1. Hand is released from the steering wheel (i.e. converted from wheel-space to car space).
  2. The hand then moves along a linear line toward the handbrake grip (this is a point that should be specified by the artist).
  3. The hand clamps to the handbrake’s grip, so that wherever the handbrake goes, the hand will follow.
  4. An animation plays to pull the handbrake in.
  5. The steps are reversed to get back to the steering wheel.

I won’t go into detail on how all this works, as it is a similar concept to the steering wheel. One thing I will say, to make it easier, there should be a control value which I’ll call handbrakeAmount. With this, you can run both the steering wheel and handbrake functionality simultaneously, yet on display the hand where it needs to be.

1
2
3
Vector3 wheelPosition = CalculateWheelHandPosition();
Vector3 handbrakePosition = CalculateHandbrakeHandPosition();
Vector3 handPosition = Vector3.Lerp(wheelPosition, handbrakePosition, handbrakeAmount);

Gear Stick

Okay, it’s starting to get complicated. We need a single hand to take care of the steering wheel, handbrake, and now the gear stick too! If you have come this far and gotten the handbrake to work, adding another control type for the hand should be simple.

The gear stick is almost identical to the handbrake.

  1. Hand is released from the steering wheel (i.e. converted from wheel-space to car space).
  2. The hand then moves along a linear line toward the gear stick grip (this is a point that should be specified by the artist).
  3. The hand clamps to the gear stick grip, so that wherever the gear stick goes, the hand will follow.
  4. An animation plays to make the gear stick go wherever it needs to..
  5. The steps are reversed to get back to the steering wheel.

There are many types of gearing systems, and having custom made IK for it could be a real pain. But if you just get the hand to where it needs to be and clamp it in place, you can let the gear stick itself drive the hand.

In terms of merging this in with our other animation, the trick is to layer lerps on top of each other. For this, we will have a wheelAmount (0 is on either of the sticks, 1 is on the wheel), and a stick amount (0 is on the gears, 1 is on the handbrake).

1
2
3
4
5
Vector3 wheelPosition = CalculateWheelHandPosition();
Vector3 gearPosition = CalculateGearHandPosition();
Vector3 handbrakePosition = CalculateHandbrakeHandPosition();
Vector3 handPosition = Vector3.Lerp(gearPosition, handbrakePosition, stickAmount);
handPosition = Vector3.Lerp(handPosition, wheelPosition, wheelAmount);

As you can hopefully see, we can get insanely complex and modular behaviour simply by layering on lerps. Getting the driver to scratch his ear, turn up the radio and take a sip from a drink would be easy (for the programmer at least).

Foot Pedals

After getting through the steering wheel, the foot pedals should be a breeze. It’s the same concept as the handbrake/gear combo, just without the steering wheel.

1
2
3
Vector3 acceleratorPosition = GetAcceleratorFootPosition();
Vector3 brakePosition = GetBrakeFootPosition();
footPosition = Vector3.Lerp(accleratorPosition, brakePosition, pedalAmount);

Once the foot is in the right position, play out the animation of the foot going down. Easy!

Body Momentum

This is a subtle effect, yet makes a significant step toward realism. When you’re driving at high speed and you slam on the brakes, you can feel your whole body shift forward as your body’s moment tries to catch up to the car’s. This can be achieved by using a spring-like effect at the base of the spine.

The basic idea of a spring is to keep a separate velocity for the springed object (the driver’s body) and have it try to keep up with its parent object (the car). And, assuming your driver is wearing a seatbelt, ensure that you clamp the final rotation.

1
2
Vector3 bodyAcceleration = (bodyAcceleration - carAcceleration) * (Time.deltaTime / smoothness);
spineRotation = Vector3.Clamp(bodyAcceleration, spineMinRotation, spineMaxRotation);

Head Look

The final, and probably easiest to do, is the head look. This is really just rotating the head bone to wherever you want it to look. You can also half rotate the neck toward the look direction to give it a more natural twist.

You might think that just looking in the forward direction of the car is enough, but you’ll find it looks pretty silly while drifting. Instead, have the head look toward the velocity of the car. If the velocity is backward, have it look toward the rear vision mirror.

1
2
3
Vector3 direction = car.velocity.normalized;
if (Vector3.Dot(direction, car.forward) < 0.1f)
	direction = (rearVisionMirror - head.position).normalized;

Conclusion

To achieve any sort of complex procedural animation, I would recommend following these steps:

  • Breakdown limbs into separate independent IK with simple controls (ideally, just a single position or scalar value).
  • Breakdown behaviour for each limb into separate actions (eg steering wheel, gear changing, braking)
  • Simulate the actions as simply as possible. Avoid working in cartesian coordinates. Instead, try to break it up into simple lerp-able values, or better yet, let an artist-made animation drive the behaviour.
  • To transition between the different actions, simulate all actions at the same time and lerp between them.


Twitter YouTube GitHub Email RSS