Migration guides

Migration guide: 0.8 to 0.9

The 0.9 update brings many breaking changes to the serialization format. These changes were necessary to solve long-standing issues with the library, and they're extensive enough that updating asset files by hand will be tedious and time-consuming.

The easiest migration path for animation assets is to remake them in the editor. I've bundled a script that will help with this, explained in the following section.

Migration through the editor

  1. I have added a migration script to contrib/migration_script.sh. When run without any arguments, this script will will

    • Create a migration-tmp-dir-RANDOM_UUID folder in the current working directory.
    • Clone and build both the v0.8 editor and v0.9 editor in that subdirectory.
    • Copy your assets folder into that subdirectory, in order to use as reference throughout the migration.
    • Launch both editors side to side. The v0.9 editor will be opened on your current assets folder, whereas the v0.8 editor will be opened on the reference copy under the migration subdirectory.

    You should copy this script somewhere convenient.

  2. Move to the root directory of the project you want to migrate. Then run

    bash path/to/migration_script.sh
    

    Two editors will open side to side.

    NOTE: If your assets directory is stored somewhere other than ./assets, you can specify that path with the -a flag, e.g.

    bash path/to/migration_script.sh -a ./custom-assets-dir
    

    NOTE: If you have already run the script before and there's already a migration-tmp-dir-RANDOM_UUID folder in your current working directory, you can continue that migration using

    bash path/to/migration_script.sh -c migration-tmp-dir-random_uuid
    

    If you don't do this and you already had started the migration previously, the current partially migrated assets folder will be copied again to the new migration directory.

  3. You can now use the old editor as a reference to recreate your animation graphs, FSMs, etc.

Migrating custom animation nodes

The NodeLike trait used for defining custom animation nodes has also been updated. In most cases the NodeLike::update logic should be unchanged, but the API for specifying the IO spec has been reworked.

The following is a (quite useless) custom animation node defined in v0.8.0:

#![allow(unused)]
fn main() {
impl NodeLike for MyCustomNode {
    fn display_name(&self) -> String {
        "Custom example node".into()
    }

    fn duration(&self, mut ctx: PassContext) -> Result<(), GraphError> {
        let back_duration = ctx.duration_back(Self::IN_TIME)?;
        ctx.set_duration_fwd(back_duration);
        Ok(())
    }

    fn update(&self, mut ctx: PassContext) -> Result<(), GraphError> {
        let input = ctx.time_update_fwd()?;
        ctx.set_time_update_back(Self::IN_TIME, input);
        let mut in_pose = ctx.data_back(Self::IN_POSE)?.into_pose().unwrap();

        // Apply some random offset to each bone every frame
        for bone in &mut in_pose.bones {
            if let Some(pos) = &mut bone.translation {
                let offset = Vec3::new(
                    rand::random::<f32>() - 0.5,
                    rand::random::<f32>() - 0.5,
                    rand::random::<f32>() - 0.5,
                ) * 0.035;

                *pos += offset;
            }
        }

        ctx.set_time(in_pose.timestamp);
        ctx.set_data_fwd(Self::OUT_POSE, in_pose);

        Ok(())
    }

    // Previously you'd specify IO specs in 4 separate methods; this makes it
    // very inconvenient if you want to define a relative ordering of IO pins
    // in order to make your graph tidier, and it's very verbose.

    fn time_input_spec(&self, _: SpecContext) -> PinMap<()> {
        [(Self::IN_TIME.into(), ())].into()
    }

    fn time_output_spec(&self, _ctx: SpecContext) -> Option<()> {
        Some(())
    }

    fn data_input_spec(&self, _: SpecContext) -> PinMap<DataSpec> {
        [(Self::IN_POSE.into(), DataSpec::Pose)].into()
    }

    fn data_output_spec(&self, _: SpecContext) -> PinMap<DataSpec> {
        [(Self::OUT_POSE.into(), DataSpec::Pose)].into()
    }
}
}

In v0.9.0 it would look like this:

#![allow(unused)]
fn main() {
impl NodeLike for MyCustomNode {
    fn display_name(&self) -> String {
        "Custom example node".into()
    }

    // `duration` and `update` now receive a `NodeContext` instead of a
    // `PassContext`

    fn duration(&self, mut ctx: NodeContext) -> Result<(), GraphError> {
        let back_duration = ctx.duration_back(Self::IN_TIME)?;
        ctx.set_duration_fwd(back_duration);
        Ok(())
    }

    fn update(&self, mut ctx: NodeContext) -> Result<(), GraphError> {
        let input = ctx.time_update_fwd()?;
        ctx.set_time_update_back(Self::IN_TIME, input);
        let mut in_pose = ctx.data_back(Self::IN_POSE)?.into_pose().unwrap();

        // Apply some random offset to each bone every frame
        for bone in &mut in_pose.bones {
            if let Some(pos) = &mut bone.translation {
                let offset = Vec3::new(
                    rand::random::<f32>() - 0.5,
                    rand::random::<f32>() - 0.5,
                    rand::random::<f32>() - 0.5,
                ) * 0.035;

                *pos += offset;
            }
        }

        ctx.set_time(in_pose.timestamp);
        ctx.set_data_fwd(Self::OUT_POSE, in_pose);

        Ok(())
    }

    // All of the IO spec is now defined in the `spec` function by calling
    // the appropriate methods on the provided `SpecContext`. The relative
    // ordering of IO pins in which you define them will be preserved in the
    // editor.

    fn spec(&self, mut ctx: SpecContext) -> Result<(), GraphError> {
        ctx.add_input_data(Self::IN_POSE, DataSpec::Pose);
        ctx.add_input_time(Self::IN_TIME);

        ctx.add_output_data(Self::OUT_POSE, DataSpec::Pose);
        ctx.add_output_time();

        Ok(())
    }
}
}