Skip to content

Tutorial: Custom Animation Module

This tutorial shows how to create a custom animation Module that computes features from motion data and integrates with the MotionEditor.


Step 1: Subclass Module

from ai4animation.Animation.Module import Module


class MyFeatureModule(Module):
    def __init__(self, motion):
        super().__init__(motion)
        self.features = self._compute(motion)

    def _compute(self, motion):
        # Precompute features from motion data
        # This runs once when the module is created
        num_frames = motion.Frames.shape[0]
        features = []
        for i in range(num_frames):
            # Example: compute center of mass height
            positions = motion.Frames[i, :, :3, 3]  # [J, 3]
            com_height = positions[:, 1].mean()
            features.append(com_height)
        return features

Step 2: Add Callback and Draw Hooks

class MyFeatureModule(Module):
    def __init__(self, motion):
        super().__init__(motion)
        self.features = self._compute(motion)

    def _compute(self, motion):
        num_frames = motion.Frames.shape[0]
        return [motion.Frames[i, :, 1, 3].mean() for i in range(num_frames)]

    def Callback(self, editor):
        # Called each frame during MotionEditor playback
        # Access current timestamp via editor
        pass

    def Draw(self, editor):
        # Render debug visualization
        if Module.Visualize[MyFeatureModule]:
            # Draw only when visualization is enabled
            pass

    def GUI(self, editor):
        # Add UI elements
        pass

Step 3: Attach to a Dataset

Modules are attached via lambda factories when creating a Dataset:

from ai4animation import Dataset, RootModule

dataset = Dataset(
    "path/to/motions",
    [
        lambda x: RootModule(x, hip, l_hip, r_hip, l_shoulder, r_shoulder),
        lambda x: MyFeatureModule(x),  # Your custom module
    ],
)

The lambda receives the Motion object and must return a Module instance. This pattern allows modules to be lazily instantiated when a motion is loaded.


Step 4: Add a Nested Series Class (Optional)

For temporal windowing, define a nested Series class extending TimeSeries:

from ai4animation.Animation.TimeSeries import TimeSeries


class MyFeatureModule(Module):
    def __init__(self, motion):
        super().__init__(motion)
        self.features = self._compute(motion)

    def _compute(self, motion):
        return [motion.Frames[i, :, 1, 3].mean() for i in range(motion.Frames.shape[0])]

    class Series(TimeSeries):
        def __init__(self, start, end, samples):
            super().__init__(start, end, samples)
            self.Heights = []

        def Draw(self):
            # Render the series data
            pass

Complete Example

from ai4animation.Animation.Module import Module
from ai4animation import Dataset, MotionEditor, AI4Animation
import os


class CenterOfMassModule(Module):
    """Tracks the center of mass height over time."""

    def __init__(self, motion):
        super().__init__(motion)
        self.com_heights = []
        for i in range(motion.Frames.shape[0]):
            positions = motion.Frames[i, :, :3, 3]
            self.com_heights.append(float(positions[:, 1].mean()))

    def Callback(self, editor):
        pass

    def Draw(self, editor):
        if Module.Visualize[CenterOfMassModule]:
            pass


class Program:
    def Start(self):
        editor = AI4Animation.Scene.AddEntity("MotionEditor")
        editor.AddComponent(
            MotionEditor,
            Dataset(
                os.path.join(ASSETS_PATH, "Motions"),
                [
                    lambda x: CenterOfMassModule(x),
                ],
            ),
            os.path.join(ASSETS_PATH, "Model.glb"),
            bone_names,
        )

    def Update(self):
        pass


if __name__ == "__main__":
    AI4Animation(Program())