Aug 2023  

Procedural animation for controlling a (simulated) robot dog: first steps

My previous post was about implementing a simulator for a quadruped robot. I got it to work, including inverse kinematics for controlling robot’s feet and torso poses. Next step is making the simulated model move and walk. How hard can it be?

procedural animation

The simplest way to generate walking motion is very similar to procedural animation. It imitates motion by setting poses of some of the character’s body parts and applying physics simulation to the rest. The character in this case is supported with external constraints, it’s more like a puppet instead of a fully simulated self-supported robot.

My idea is to use procedurally animated model to indirectly drive the “real” robot model. Just need to measure angles of the animated model’s joints, and use this angles as targets for motors of the actual simulated robot’s model.

stepping in place

Let’s start with just stepping in place. If robot’s legs are controlled with inverse kinematics, the the only thing needed is to raise and lower the feet. For the trot gait[1], only one of the diagonal pairs of feet is moved, while another pair stays on the ground.

This can be managed with a simple state machine. Each of the feet goes through a sequence of states: ground→lift→plant→ground. Diagonal pairs of feet go into this sequence alternatively on every other step. Here is how the code looks:

export class UnitreeA1 {
    constructor() {
        [...]
        // identifiers for the feet: front left, rear right...
        this.feet_names =
            ["FL_foot", "FR_foot", "RL_foot", "RR_foot"];
        this.feet_states = {}; // structure for feet state machines
        for (let f of this.feet_names) {
            this.feet_states[f] = {
                id: "ground", // ground, lift, plant
                goal: false,  // target point for moving the foot 
                goal_distance: 0,
                name: f
            }
        }
        this.step_i = 0;
    }

   step() {
        let w = new THREE.Vector3();

        // count number of feet on the ground
        let n_ground = 0;
        for (let j of this.feet_names) {
            if (this.feet_states[j].id == "ground") n_ground++;
        }
        // only start the next step when all 4 feet are on the ground
        if (n_ground == 4) this.step_i++;

        // select which feet to move
        let selected = [];
        if (this.step_i % 2 == 0) {
            // on even steps, select front left and rear right feet
            selected[0] = this.feet_states["RR_foot"];
            selected[1] = this.feet_states["FL_foot"];
        } else {
            // on odd steps, select front right and rear left feet
            selected[0] = this.feet_states["RL_foot"];
            selected[1] = this.feet_states["FR_foot"];
        }

        if (n_ground == 4) { // when all 4 feet are on the ground
            // start the next step: put selected feet into the "lift" state
            for (let s of selected) {
                s.id = "lift";
                // set target point for lifting as default stading position
                // raised 0.1m above ground
                this.step_targets[s.name].getWorldPosition(w); 
                s.goal = w.clone();
                s.goal.y = 0.1; // raise the root
            }
        }

        // move feet positions towards goal points, by 0.05m at a time
        for (let j of this.feet_names) { 
            this.feet_states[j].goal_distance = 
                move_towards(this.targets[j].position, 
                             this.feet_states[j].goal, 0.05)
        }

        // if all feet have reached the goal points
        if (selected.every(v => v.goal_distance < 0.001)) {
            if (selected.every(v => v.id == "lift")) {
                // if reached the "lift" goal, switch to "plant"
                for (let s of selected) {
                    // the goal for planting the foot is detault standing position
                    // s.goal = this.step_targets[s.name].position.clone();
                    this.step_targets[s.name].getWorldPosition(w); 
                    s.goal = w.clone();
                    s.goal.y = 0.02;
                    s.id = "plant";
                }
            } else if (selected.every(v => v.id == "plant")) {
                // if reached the "plant" goal, the foot is on the ground 
                for (let s of selected) {
                    s.id = "ground";
                }
            }
        }
    }
    [...]

step() is called at 30Hz. Here is how the resulting motion looks:

There are two robot models in the video, model on the left is animated by setting it’s feet positions. Then it’s joints angles are transferred to the model on the right, which is the unconstrained full simulation model.

The simulation model bounces after stepping. Rapier.js physics engine supports only rigid bodies simulation, so the feet collide with the ground hard. In the real robot, feet pads are elastic, which provides damping. A quick fix is to add a short delay after planting the foot, at least it allows the model to settle. Here is how the model with the delay looks, in this case bouncing is reduced:

So this works well, but what happens when the robot tries to move forward while stepping in place? It will tilt and fall:

Code is on github, and a live demo is below:

Next, implementing walking motion generation.


  1. Or, in case of stepping in place, this can be called a piaffe: "The piaffe (French pronunciation: [pjaf]) is a dressage movement where the horse is in a highly collected and cadenced trot, in place or nearly in place" ↩︎