Model editing

The most general way to create an MjModel instance is by loading an XML file via MjModel::from_xml. Due to MjModel only allowing (some) changes to parameters and not to the actual geometry, MuJoCo introduced Model Editing.

In MuJoCo-rs, we created a high-level wrapper around MuJoCo’s C API, which provides safe wrappers around C structs, as well as methods. Aside to that, we try to stay faithful to MuJoCo’s implementation.

A procedurally generated, not yet compiled, model is represented by its specification (MjSpec). A specification can be created empty with MjSpec::new or pre-filled from XML, with:

After creation, we can use MjSpec to add items to the model, such as joints, geoms, actuators, etc. Non-structured items can be added through MjSpec itself (e.g., actuators, sensors, meshes, etc.). Structured items can be added through MjsBody (e.g., bodies, geoms, joints, etc.).

After procedurally creating a specification with MjSpec, the latter can either be compiled for direct use in the simulation or saved to an XML file.

Basic editing

Let’s lead with an example. We will create a model, where a ball falls onto a plane. We start by creating an MjSpec instance:

fn main() {
    let mut spec = MjSpec::new();
}

Now, we need to create a spherical body, which will be our ball. This also includes adding a spherical geom and a free joint. Since bodies are structured elements, we can’t add them to MjSpec. Instead, we will add them to the world body (the worldbody element in a model’s XML).

To access the specification’s world body, we can use the MjSpec::world_body method. This method returns an object that acts like a reference to the world body, but is actually a struct wrapping the underlying mutable pointer to the FFI type mujoco_c::mjsBody. Because it isn’t a true Rust reference, any variable holding this “reference-like” object must itself be declared mutable in order to modify the world body. The latter is also true for other model editing wrapper types.

fn main() {
    let mut spec = MjSpec::new();
    let mut world = spec.world_body();       // or spec.body("world").unwrap();
}

We can now add our ball’s body, geom and joint like so:

fn main() {
    let mut spec = MjSpec::new();
    let mut world = spec.world_body();       // or spec.body("world").unwrap();

    // Add the ball
    let mut ball_body = world.add_body()
        .with_name("ball")                   // name
        .with_pos([0.0, 0.0, 1.0]);          // position

    ball_body.add_geom()
        .with_size([0.010, 0.0, 0.0])        // set the radius to 10 mm.
        .with_type(MjtGeom::mjGEOM_SPHERE);  // make this a spherical geom (default).

    ball_body.add_joint()
        .with_type(MjtJoint::mjJNT_FREE);    // make the ball free to move anywhere.
}

Tip

In the above block, we used methods that have the with_ prefix. These consume the struct instance, set the corresponding attribute and in the end return the consumed instance back to the caller. Thus, they facilitate a builder-style API, where methods can be chained together to build the instance. Alternatively, methods that have the set_ prefix can be used, which don’t consume the instance. Setter (set_) methods exists only for simple types. Anything more complex can be modified through getters, which end with the _mut suffix.

Finally, we can now add the base plane, like so:

fn main() {
    let mut spec = MjSpec::new();
    let mut world = spec.world_body();       // or spec.body("world").unwrap();

    // Add the ball
    let mut ball_body = world.add_body()
        .with_name("ball")                   // name
        .with_pos([0.0, 0.0, 1.0]);          // position

    ball_body.add_geom()
        .with_size([0.010, 0.0, 0.0])        // set the radius to 10 mm.
        .with_type(MjtGeom::mjGEOM_SPHERE);  // make this a spherical geom (default).

    ball_body.add_joint()
        .with_type(MjtJoint::mjJNT_FREE);    // make the ball free to move anywhere.

    // Add the base plane
    world.add_geom()
        .with_type(MjtGeom::mjGEOM_PLANE)
        .with_size([1.0, 1.0, 1.0]);
}

This concludes specification’s definition. We can now compile it to a model, which can then be saved to either an MJCF (XML) file or to an MJB (binary) file:

fn main() {
    let mut spec = MjSpec::new();
    let mut world = spec.world_body();       // or spec.body("world").unwrap();

    // Add the ball
    let mut ball_body = world.add_body()
        .with_name("ball")                   // name
        .with_pos([0.0, 0.0, 1.0]);          // position

    ball_body.add_geom()
        .with_size([0.010, 0.0, 0.0])        // set the radius to 10 mm.
        .with_type(MjtGeom::mjGEOM_SPHERE);  // make this a spherical geom (default).

    ball_body.add_joint()
        .with_type(MjtJoint::mjJNT_FREE);    // make the ball free in all directions.

    // Add the base plane
    world.add_geom()
        .with_type(MjtGeom::mjGEOM_PLANE)
        .with_size([1.0, 1.0, 1.0]);

    // Compile and save
    let model = spec.compile().expect("failed to compile");
    spec.save_xml("model.xml").expect("failed to save");     // save XML.
    model.save(Some("filename"), None);                      // save binary.
}

The model from the above example, generated by MjSpec::compile, can be used exactly the same as if we were to directly load an XML model (see Basic simulation).

Iterators

Since MuJoCo-rs 1.5.0, it is possible to also iterate existing MjSpec items (geoms, joints, etc.). Iterators exist on MjSpec and MjsBody.

To iterate over MjSpec items, call [item_type]_iter_mut, with [item_type] replaced by geom, body, etc.

...
for body in spec.body_iter_mut() {  // spec is MjSpec.
    println!("{}", body.name());
}
...

Iteration over MjsBody items can be used in a similar way. The only difference is an additional boolean parameter, which enables recursive iteration when true.

...
// Iterate top level bodies of body.
for body in body.body_iter_mut(false) {  // body is MjsBody.
    println!("{}", body.name());
}
...
...
// Iterate top level bodies of body + their sub-bodies recursively.
for body in body.body_iter_mut(true) {  // body is MjsBody.
    println!("{}", body.name());
}
...

Attention

Since all the underlying C functions require mutable pointers, the iterators also make a mutable borrow. As a result MjSpec cannot be read during iteration. Iterated elements can however be modified.

Examples

Additional examples on model editing are available in repository’s examples: