Procedural Animation for Characters via Scripting in C#

Replace tedious animation cycles by learning how to generate physically simulated behaviors via scripting in Unity!

kick-830w-1

This blog post will serve as an introduction to one of the multiple procedural animation methods supported by DeepMotion Avatar: procedural animation for various character standing movements via C# scripting in the Unity editor.

Now that you have constructed a physically simulated character that can stand in-place, self-balance, and walk what’s next? This tutorial shows you how you can drive your characters with unique, interactive, physically simulated actions via scripting. This is an more advanced tutorial that requires some knowledge of C# and basic math.

In this post, we will cover 3 motions (a basic squat animation, a waist rotation animation, and a kicking animation) that we have made with this process, and go over the steps for creating a new motion from scratch. All the animations created using this technique will be completely procedural, physics-based movements. (DeepMotion Neuron, to be released later this year, will make make these procedural, physics-based movements even more natural looking, using animation data to train characters motor skills prior to run-time.)

Physics Based Procedural Animation for Characters

The goal behind using procedural animation to move a character is to replace tedious keyframe animation cycles and to make characters interactive. Characters created using Avatar can be animated in this way because they are physically simulated with a multitude of biomechanical parameters. This allows for the procedural animation of characters using simple scripting to puppeteer the character’s physical body. We use C# to modify the controller parameters and desired pose to change the body positioning. The difference between this type of animation and traditional keyframe animation is that instead of controlling the position and location of each body part, you control the motion of each joint motor. This results in an animation that can adapt to how the rest of the body is being interacted with.

The Avatar Runtime package doesn’t have a keyframe timeline of traditional animation, so let’s craft each of the Avatar character’s motions using C# code. We use this code tell the motor what angle we want the joint to reposition to and the Avatar character controller will take care of the rest.

Each command given to the controller is done as part of a coroutine, so we need to tell the controller to ignore other commands until the current animation is finished to prevent conflicting commands being given to the same limb. This is not required when animating an Avatar character, but it is highly recommended. Additional parameters to specify is the step interval duration between each command and store the old position are required.

The functions you will be using for the majority of your animation scripts are:

tntController.m_controlParams.m_params[tntControllerParams.P_PARAMNAME] = parameterValue;
tntController.m_desiredPose.SetJointOrientationToEulers(jointIndex, jointOrientation);

When creating these animations, start by obtaining the joint index for each joint you will be editing in the animation. The desired pose controller also needs to allow editing as eulers to use Vector3 for controlling rotation. You can get the joint index via the joint name and allow editing as eulers by accessing the desired pose script object with the joint index:

int m_jointName = m_rootTntBase.NameToIndex(“jointName”);
if (m_jointName >= 0)
	m_controller.m_desiredPose.m_editAsEulers[m_jointName] = true;

A Basic Squat Animation

The first animation we are going to cover is a traditional squatting motion. The character is going to squat on keypress, hold the position for about a second, and then stand up.

squat-830w

We start the animation by telling the controller that we are starting an animation and saving the old parameters that we will be modifying, since we want to have the character revert to its original position after the animation is completed. Also stored at the beginning are a few parameters we use for calculating the in-between steps that we can use to slow down the motion, and the aforementioned step interval.

For the squat animation, we are going to save the parameters for body frame lean forward, desired height, left shoulder position, right shoulder position, and gyro ratio. We also need a desired height and lean for the final position of the squat, as well as calculating and storing the deltas necessary for providing in-between positions to slow down the animation.

New Values

  • destination height: 0.6f
  • destination lean: 0.3f
  • delta height: destination height - original desired height
  • delta lean: destination lean - original body frame lean
  • delta shoulder lift: 105. This value is the destination shoulder orientation as well, we assume that the shoulders start at a relaxed state of 0 for orientation

The first step of this animation is to prevent the Avatar character controller from using the arms since we want to position the arms in the correct position for a squat. We continue by adjusting the gyro ratio to keep the character stable while trying to squat, setting the desired values to half of the way to the goal position. This script sets the desired_height and bframe_lean_forward (body frame) parameters and the desired pose for the shoulders. After setting the positions for the character, we need to put a delay in before setting the next position, which is why we use coroutines.

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_DESIRED_HEIGHT] = desiredHeightOld + deltaHeight / 2;

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = bframeLeanForwardOld + deltaLean / 2 ;
m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(-deltaShoulderLift / 2, 0, 0));
m_controller.m_desiredPose.SetJointOrientationToEulers(m_rShoulder, rShoulderOld + new Vector3(-deltaShoulderLift / 2, 0, 0));

We wait for a duration of time since physically simulated motions are not instantaneous. If we do not wait, we will be attempting to set a new position before the character finishes reaching the previous one, which would look similar to skipping a keyframe in traditional animation. Here we set the character to the final position for the squatting pose. Notice that we don’t have to animate the knee joints since the character controller understands how to properly control the legs when the desired height is lowered.

yield return new WaitForSeconds(stepInterval);

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_DESIRED_HEIGHT] = destHeight;

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = destLean;

m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(-deltaShoulderLift, 0, 0));

m_controller.m_desiredPose.SetJointOrientationToEulers(m_rShoulder, rShoulderOld + new Vector3(-deltaShoulderLift * 1.1f, 0, 0));

Prior to having the character stand up, we again put in a delay, but this time to have the character rest in the squatting pose for a short duration.

yield return new WaitForSeconds(stepInterval * 5);

Next we want the character to stand up, but since it looks more natural for the standing up motion to be slower, we will be using 4 in-between steps instead of 1. We use multipliers on the height delta and consistent time delays (stepInterval / 2) to get even spacing between each position, except for the final in between position, where we decrease the spacing between positions to get a smooth slowdown that you would see a normal person do.

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_DESIRED_HEIGHT] = desiredHeightOld + deltaHeight * 0.75f;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = bframeLeanForwardOld + deltaLean * 0.75f  ;
m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(-deltaShoulderLift * 0.75f, 0, 0));
m_controller.m_desiredPose.SetJointOrientationToEulers(m_rShoulder, rShoulderOld + new Vector3(-deltaShoulderLift * 0.75f, 0, 0));
yield return new WaitForSeconds(stepInterval / 2);

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_DESIRED_HEIGHT] = desiredHeightOld + deltaHeight * 0.5f;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = bframeLeanForwardOld + deltaLean * 0.5f  ;
m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(-deltaShoulderLift * 0.5f, 0, 0));
m_controller.m_desiredPose.SetJointOrientationToEulers(m_rShoulder, rShoulderOld + new Vector3(-deltaShoulderLift * 0.5f, 0, 0));
yield return new WaitForSeconds(stepInterval / 2);

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_DESIRED_HEIGHT] = desiredHeightOld + deltaHeight * 0.25f;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = bframeLeanForwardOld + deltaLean * 0.25f  ;
m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(-deltaShoulderLift * 0.25f, 0, 0));
m_controller.m_desiredPose.SetJointOrientationToEulers(m_rShoulder, rShoulderOld + new Vector3(-deltaShoulderLift * 0.25f, 0, 0));
yield return new WaitForSeconds(stepInterval / 2);

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_DESIRED_HEIGHT] = desiredHeightOld + deltaHeight * 0.05f;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = bframeLeanForwardOld + deltaLean * 0.05f  ;
m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(-deltaShoulderLift * 0.05f, 0, 0));
m_controller.m_desiredPose.SetJointOrientationToEulers(m_rShoulder, rShoulderOld + new Vector3(-deltaShoulderLift * 0.05f, 0, 0));
yield return new WaitForSeconds(stepInterval / 2);

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_DESIRED_HEIGHT] = desiredHeightOld;
m_controller.m_controlParams.m_params[tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = bframeLeanForwardOld;
m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld);
m_controller.m_desiredPose.SetJointOrientationToEulers(m_rShoulder, rShoulderOld);

After we finish the motion, we make sure to give the controller control over the shoulders again, and to revert the gyro ratio back to normal. The delay between the motion change and the gyro change is to allow the controller to adjust to the standing position first, without it the character will tend to stumble.

m_controller.EnableLeftShoulderSwing(true);
m_controller.EnableRightShoulderSwing(true);
yield return new WaitForSeconds(stepInterval * 5);
m_controller.m_controlParams.m_params[tntHumanoidControlParams.P_GYRO_RATIO] = gyroOld;

After we finish the animation, we need to update the controller so it can allow player control and other animations to trigger. All animations should start and end in the same way, to avoid conflict with other animations trying to control the same bones.

m_animationInProgress = false;
m_allowDirectInputControl = true;

A Waist Rotation Animation

This next animation is a more dynamic animation, involving a continuous motion instead of transitioning between two static poses. Like the previous animation, we start off by saving a few values and setting a few destination goals, as well as the interval between positions.

waist-rotate-830w

We will store the old bframe_lean_forward, bframe_lean_sideways, desired_height, and gyro_ratio, where bframe stands for body frame.

New Values

  • destination lean forward: 0.6f
  • destination lean sideways: 0.6f
  • delta lean forwards: destination forward lean - old forwards lean
  • delta lean sideways: destination sideways lean - old sideways lean

We also set a stepInterval value of 0.15f to use as the delay between each new position setting. Start the animation by adjusting the desired height and gyro ratio to allow the character to rotate its torso smoothly.

m_controller.m_controlParams.m_params[tntHumanoidControlParams.P_DESIRED_HEIGHT] = 1.1f;
m_controller.m_controlParams.m_params[tntHumanoidControlParams.P_GYRO_RATIO] = 0.1f;

Since we are making a repetitive motion, we can use a loop to repeat the motion a few times before stopping it.

for (int i = 0; i < 3; i++) {
…
}

For a smooth motion, we’re going to use an even interval and spacing between each position. We only need to adjust the lean forwards and lean sideways parameters to get the waist rotation to work. Start with a lean forward, keeping the sideways lean at the initial value. Continue onto the position in between the sideways lean and the forward lean, to produce a more circular motion. The third position will be the full sideways lean, where we will also make sure to reset the forward lean to the original position. Continue along with the rest of the animation, moving to the backwards lean and the other sideways lean. End the animation cycle on the step before the initial forward lean to create a looping animation. The following code goes through the entire waist rotation animation, and would be placed inside the above for loop.

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS] = leanSideWaysOld;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = leanForwardOld - deltaLeanForward;
yield return new WaitForSeconds (stepInterval);

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = leanForwardOld - 0.7f * deltaLeanForward;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS] = leanSideWaysOld - 0.7f * destLeanSideways;
yield return new WaitForSeconds (stepInterval);

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = leanForwardOld;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS] = leanSideWaysOld - destLeanSideways;
yield return new WaitForSeconds (stepInterval);

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = leanForwardOld + 0.7f * deltaLeanForward;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS] = leanSideWaysOld - 0.7f * destLeanSideways;
yield return new WaitForSeconds (stepInterval);

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = leanForwardOld + deltaLeanForward;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS] = leanSideWaysOld;
yield return new WaitForSeconds (stepInterval);

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = leanForwardOld + 0.7f * deltaLeanForward;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS] = leanSideWaysOld + 0.7f * destLeanSideways;
yield return new WaitForSeconds (stepInterval);

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = leanForwardOld;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS] = leanSideWaysOld + destLeanSideways;
yield return new WaitForSeconds (stepInterval);

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS] = leanSideWaysOld + 0.7f * deltaLeanForward;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = leanForwardOld - 0.7f * deltaLeanForward;
yield return new WaitForSeconds (stepInterval);

After looping through the animation 3 times, reset to the original position prior to starting the animation and the original parameters. Don’t forget to tell the controller the animation is finished!

m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_FORWARD] = leanForwardOld;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS] = leanSideWaysOld;
m_controller.m_controlParams.m_params[tntHumanoidControlParams.P_DESIRED_HEIGHT] = desiredHeightOld;
m_controller.m_controlParams.m_params [tntHumanoidControlParams.P_GYRO_RATIO] = gyroOld;

m_animationInProgress = false;
m_allowDirectInputControl = true;

A Kicking Animation

This animation is much more complex and and combines elements from the squatting animation and a new process utilizing the multipoint tracking ability of the controller. Instead of individually animating the legs, we will utilize the foot tracker to let the controller command the foot using inverse dynamics.

kick-830w

Before starting work on this animation, start by creating two empty game objects to use for the foot tracker transforms. Move each of these new game objects and position them in the center of the mass of each foot. Select the character, expand the “root” game object, and open up the “HumanoidController” game object. Assign these game objects to the corresponding left/right foot tracker fields to use them as trackers.

foot-trackers

Like with previous animations, we start with saving a few old parameters and creating a few goal values.

Saved Values

  • gyro_ratio
  • left shoulder euler angle
  • right shoulder euler angle
  • left foot tracker parent game object
  • left foot tracker old local position
  • left foot tracker old global position

New Values

  • kick distance: 1f
  • delta shoulder lift: 115f

We will also set a stepInterval value of 0.12f since we want a more rapid motion for the kick. Before we start the animation, we need to ensure that the left foot tracker exists as this is how we will create the kicking motion. For this animation we are going to use 4 interval steps on the up kick, and 3 interval steps to bring the leg back down.

We’ll take control of the shoulders before starting the animation and have the character raise its arms while kicking. Step interval timing and separation variations are used to produce motion similar to how animation curves work. As you see in the code below, we have the arms reach the high point before the leg finishes kicking, as that looks much more natural.

leftFootTracker.transform.localPosition = footPosOrigin + new Vector3 (0f, kickDistance * 0.1f, 0f);
m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(0, 0, deltaShoulderLift * 0.25f));
m_controller.m_desiredPose.SetJointOrientationToEulers(m_rShoulder, rShoulderOld + new Vector3(0, 0, -deltaShoulderLift * 0.25f));
yield return new WaitForSeconds(stepInterval * 2);

leftFootTracker.transform.localPosition = footPosOrigin + new Vector3 (0f, kickDistance * 0.25f, kickDistance * 0.1f);
m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(0, 0, deltaShoulderLift * 0.5f));
m_controller.m_desiredPose.SetJointOrientationToEulers(m_rShoulder, rShoulderOld + new Vector3(0, 0, -deltaShoulderLift * 0.5f));
yield return new WaitForSeconds(stepInterval);

leftFootTracker.transform.localPosition = footPosOrigin + new Vector3 (0f, kickDistance * 0.5f, kickDistance * 0.25f);
m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(0, 0, deltaShoulderLift));
m_controller.m_desiredPose.SetJointOrientationToEulers(m_rShoulder, rShoulderOld + new Vector3(0, 0, -deltaShoulderLift));
yield return new WaitForSeconds(stepInterval);

leftFootTracker.transform.localPosition = footPosOrigin + new Vector3 (0f, kickDistance * 0.75f, kickDistance * 0.5f);
yield return new WaitForSeconds(stepInterval / 8);

A very long distance between each position step to create the sharp, snappy ending of the kick. The delay after is longer since the leg has to travel much further between desired positions.

leftFootTracker.transform.localPosition = footPosOrigin + new Vector3 (0f, kickDistance * 1.6f, kickDistance * 0.9f);
yield return new WaitForSeconds(stepInterval * 2);

After reaching the peak of the kick we need to bring the foot down to the original position. We choose to let the foot reach the ground before changing the position of the arms again to produce a more natural looking kick. An even step interval is used to produce a linear motion.

leftFootTracker.transform.localPosition = footPosOrigin + new Vector3 (0f, kickDistance * 0.75f, kickDistance * 0.5f);
yield return new WaitForSeconds(stepInterval);

leftFootTracker.transform.localPosition = footPosOrigin + new Vector3 (0f, kickDistance * 0.5f, kickDistance * 0.25f);
yield return new WaitForSeconds(stepInterval);

leftFootTracker.transform.localPosition = footPosOrigin + new Vector3 (0f, kickDistance * 0.25f, kickDistance * 0.1f);
yield return new WaitForSeconds(stepInterval);

leftFootTracker.transform.position = footPosOld;
leftFootTracker.transform.parent = oldParent;

m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld);
m_controller.m_desiredPose.SetJointOrientationToEulers(m_rShoulder, rShoulderOld);

The kicking animation could have been done using the desired pose controller for the leg, but we chose to use the leg trackers to show how this could be integrated with a multipoint tracking setup if a user didn’t want to perform the kicking action themselves.

Creating a New Animation

Here we’ll go over the steps for creating a new animation from scratch. For this portion of the tutorial, we’re going to make a simple hand waving animation.

A typical hand waving motion consists of pivoting the arm around the shoulder and having a looser elbow and wrist to emphasize that motion. We could use the multipoint tracking system to create this motion like with the kick, but we will use the desired pose method instead.

Saved Values

  • bodyframe sideways lean
  • left shoulder orientation
  • left elbow orientation
  • left wrist orientation

New Values

  • left shoulder angle X
  • left shoulder angle Y
  • left shoulder angle Z
  • left elbow angle Y (the hinge joint here uses the Y-axis)
  • left wrist angle Y
  • step interval

We will use the above values to control the waving motion. We’ll also make sure to tell the controller we want control of the left shoulder and that we have an animation in progress. It make take a few tries to get the angles correct for your character, but these ones work for the example robot.

m_allowDirectInputControl = false;
m_animationInProgress = true;

float leanSidewaysOld = m_controller.m_controlParams.m_params[tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS];
Vector3 lShoulderOld = m_controller.m_desiredPose.GetJointOrientation(m_lShoulder).eulerAngles;
Vector3 lElbowOld = m_controller.m_desiredPose.GetJointOrientation(m_lElbow).eulerAngles;
Vector3 lWristOld = m_controller.m_desiredPose.GetJointOrientation(m_lWrist).eulerAngles;

float deltaLShoulderLift = 140f;
float lShoulderVariation = 30f;
float lElbowVariation = 45f;
float lWristVariation = 10f;
float destLeanSideways = 0.3f;
float stepInterval = 0.2f;
float lShoulderRotate = -90f;
float lShoulderTilt = -35f;
Vector3 destLShoulder = lShoulderOld + new Vector3(lShoulderTilt, lShoulderRotate, deltaLShoulderLift);

m_controller.EnableLeftShoulderSwing(false);

After saving the old values and creating some new ones, we’ll start off the waving action with a slight lean to the side for balance and a raising of the arm. Remember that a wave isn’t just motion in the shoulder, which is why we included offsets for the wrist and elbow motions. Due to the elbow being a hinge joint, we need to properly rotate the arm and adjust our rotation angles to match.

m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(0, 0, deltaLShoulderLift * 0.25f));
m_controller.m_controlParams.m_params[tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS] = leanSidewaysOld + (0.5f * destLeanSideways);
yield return new WaitForSeconds(stepInterval);

m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(0, 0, deltaLShoulderLift * 0.6f));
m_controller.m_controlParams.m_params[tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS] = leanSidewaysOld + destLeanSideways;
yield return new WaitForSeconds(stepInterval);

m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(0, 0, deltaLShoulderLift));
yield return new WaitForSeconds(stepInterval);

m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(lShoulderTilt, lShoulderRotate * 0.5f, deltaLShoulderLift));
yield return new WaitForSeconds(stepInterval / 2f);

m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, destLShoulder);

After positioning the arm correctly into a raised position, we can create a repetitive arm motion using a for loop. Even though we used the Z-axis on the shoulder to raise the arm, we now use the X-axis to do the shoulder motions for the wave. This is because we rotated the shoulder by 90 degrees along the Y-axis.

for (int i = 0; i < 3; i++)
{
    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, destLShoulder + new Vector3(lShoulderVariation, 0, 0));
    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lElbow, lElbowOld + new Vector3(0, -lElbowVariation * 0.5f, 0));
    yield return new WaitForSeconds(stepInterval / 2f);

    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lElbow, lElbowOld + new Vector3(0, -lElbowVariation, 0));
    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lWrist, lWristOld + new Vector3(0, lWristVariation, 0));
    yield return new WaitForSeconds(stepInterval / 2f);

    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lElbow, lElbowOld + new Vector3(0, -lElbowVariation * 0.5f, 0));
    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lWrist, lWristOld);
    yield return new WaitForSeconds(stepInterval / 2f);

    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, destLShoulder);
    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lElbow, lElbowOld);
    yield return new WaitForSeconds(stepInterval / 2f);

    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, destLShoulder + new Vector3(-lShoulderVariation, 0, 0));
    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lElbow, lElbowOld + new Vector3(0, lElbowVariation * 0.5f, 0));
    yield return new WaitForSeconds(stepInterval);

    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lElbow, lElbowOld + new Vector3(0, lElbowVariation, 0));
    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lWrist, lWristOld + new Vector3(0, -lWristVariation, 0));
    yield return new WaitForSeconds(stepInterval / 2f);

    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lElbow, lElbowOld + new Vector3(0, lElbowVariation * 0.5f, 0));
    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lWrist, lWristOld);
    yield return new WaitForSeconds(stepInterval / 2f);

    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, destLShoulder);
    m_controller.m_desiredPose.SetJointOrientationToEulers(m_lElbow, lElbowOld);
    yield return new WaitForSeconds(stepInterval / 2f);
}

After finishing the wave, we need to bring the hand back down before we can let the controller take over the arm. To bring the arm back smoothly we need to retrace the steps we took to raise the arm, but we can skip a few of the smaller intermediate steps. After finishing the animation, don’t forget to:

m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(0, lShoulderRotate * 0.5f, deltaLShoulderLift));
yield return new WaitForSeconds(stepInterval / 2f);

m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld + new Vector3(0, 0, deltaLShoulderLift * 0.6f));
m_controller.m_controlParams.m_params[tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS] = leanSidewaysOld + destLeanSideways;
yield return new WaitForSeconds(stepInterval);

m_controller.m_desiredPose.SetJointOrientationToEulers(m_lShoulder, lShoulderOld);
m_controller.m_controlParams.m_params[tntHumanoidControlParams.P_BFRAME_LEAN_SIDEWAYS] = leanSidewaysOld;

m_controller.EnableLeftShoulderSwing(true);
m_animationInProgress = false;
m_allowDirectInputControl = true;

Troubleshooting and Iterating

When creating motions you’ll tend to notice that simple actions are a lot more complex than they first seem to be. The waving motion we created was more complicated than we orignally thought and took some iteration; in order to have the hand facing and the elbow bending in the right way the complexity of this action was greatly increased.

Initially when creating the animation we tried to create a waving motion without rotating the arm, but this method didn’t allow the elbow to help with the waving action.

waving-failure1-830w

Next, we tried to quickly rotate the arm after raising it, but due to joint limits and the way that the arm worked, it simply flopped forwards.

waving-failure2-830w

Next, we figured out that we had to simultaneously rotate the X-axis while rotating the arm to position the elbow and hand correctly. But since we hadn’t accounted for the rotation of the arm, the previous waving that worked moved the arm in the wrong direction.

waving-failure3-830w

Creating the exact motion pictured in your head will take some time, but with a bit of math and time you can create natural looking, interactive, procedural animations too.

waving-final-830w

Sign up for DeepMotion Avatar to create your own physics-based procedural animations. Already have an account? You can ask additional questions and share your own creations on our Forum!


DeepMotion is working on core technology to transform traditional animation into intelligent simulation. Through articulated physics and machine learning, we help developers build lifelike, interactive, virtual characters and machinery. Many game industry veterans remember the days when NaturalMotion procedural animation used in Grand Theft Auto was a breakthrough from IK-based animation; we are using deep reinforcement learning to do even more than was possible before. We are creating cost-effective solutions beyond keyframe animation, motion capture, and inverse kinematics to build a next-gen motion intelligence for engineers working in VR, AR, robotics, machine learning, gaming, animation, and film. Interested in the future of interactive virtual actors? Learn more here or sign up for our newsletter.