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).

Examples

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