Load and Play Motion

All motion loaders return the same ke.animation.SkeletonMotion type. This lets playback, sampling, analysis, visualization, and BVH export operate on a motion without depending on its source file format.

Load motion

from pathlib import Path

import kangengine as ke

# BVH hierarchy and animation
bvh_motion: ke.animation.SkeletonMotion = (
    ke.asset.BVHLoader.load_motion(bvh_path="walk.bvh", scale=1.0)
)

# One animation clip from an FBX character
fbx_motion: ke.animation.SkeletonMotion = ke.asset.FBXLoader.load_motion(
    fbx_path="character.fbx",
    clip_index=0,
    fps=60.0,
    scale=0.01,
)

# AMASS poses mapped onto a selected body-model skeleton
model_path: Path = ke.asset.smpl.repository_smplx_model_path(gender="neutral")
smplx_model: ke.asset.SMPLXModel = ke.asset.SMPLXModel.load(path=model_path)
smplx_body: ke.asset.SMPLXBody = smplx_model.create_body()
amass_motion: ke.animation.SkeletonMotion = ke.asset.AMASSLoader.load_motion(
    path="walking_poses.npz",
    skeleton_tree=smplx_body.skeleton_tree,
)

Use load_skeleton() when only the hierarchy is needed. Use parse() when import diagnostics or source metadata are also needed:

result: ke.asset.BVHImportResult = ke.asset.BVHLoader.parse(
    bvh_path="walk.bvh"
)
print(result.motion.num_frames(), result.frame_rate)
for warning in result.diagnostics.warnings:
    print(warning)

View BVH motion

motion: ke.animation.SkeletonMotion = ke.asset.BVHLoader.load_motion(
    bvh_path=bvh_file,
    scale=scale,
)
editor: ke.motion_module.MotionEditor = ke.motion_module.MotionEditor(
    motion=motion,
    motion_name=motion.motion_name(),
)

config: ke.visual.SkeletalVisualConfig = ke.visual.SkeletalVisualConfig()
skeleton: ke.visual.SkeletalVisual = ke.visual.SkeletalVisual.define(
    app=app,
    material=material,
    path="/bvh_skeleton",
    motion=motion,
    time=0.0,
    loop=True,
    config=config,
)

Update playback in the application loop:

if editor.update(dt=app.get_delta_time()):
    skeleton.apply_motion(
        motion=motion,
        time=editor.player.time,
        loop=editor.player.loop,
    )

Run:

python ./python/examples/view_bvh_character.py /path/to/motion.bvh
Complete source: view_bvh_character.py
  1"""View a BVH motion with the C++ SkeletalVisual."""
  2
  3from __future__ import annotations
  4
  5import argparse
  6from pathlib import Path
  7
  8import kangengine as ke
  9from kangengine import asset, imgui, visual
 10
 11
 12def repo_root() -> Path:
 13    return Path(__file__).resolve().parents[2]
 14
 15
 16def default_bvh_file() -> Path:
 17    return (
 18        repo_root()
 19        / "assets"
 20        / "external"
 21        / "SMPL_AMASS_T_HDM_bk_01-01_01_120_poses.bvh"
 22    )
 23
 24
 25class BVHCharacterViewer(ke.App):
 26    def __init__(self, bvh_file: Path, scale: float):
 27        super().__init__()
 28        self.bvh_file = str(bvh_file)
 29        self.scale = scale
 30
 31    def setup(self):
 32        self.show_skeleton = True
 33        self.show_joints = True
 34
 35        self.standard_materials = self.create_standard_materials()
 36        self.skeleton_material = self.standard_materials.common
 37        self.add_ground(material=self.standard_materials.ground)
 38        self.set_camera_view([0.0, 1.2, 3.0], [0.0, 0.9, 0.0])
 39
 40        self.motion = asset.BVHLoader.load_motion(self.bvh_file, self.scale)
 41        motion_name = Path(self.motion.motion_name() or self.bvh_file).name
 42        self.editor = ke.motion_module.MotionEditor(
 43            self.motion, motion_name=motion_name
 44        )
 45
 46        config = visual.SkeletalVisualConfig()
 47        config.bone_radius = 0.03
 48        config.joint_radius = 0.025
 49        config.visible = self.show_skeleton
 50        config.show_joints = self.show_joints
 51        self.skeleton_visual = visual.SkeletalVisual.define(
 52            app=self,
 53            material=self.skeleton_material,
 54            path="/bvh_skeleton",
 55            motion=self.motion,
 56            time=0.0,
 57            loop=True,
 58            config=config,
 59        )
 60
 61        print(
 62            f"BVH loaded: {Path(self.bvh_file).name} "
 63            f"joints={self.motion.num_joints()} "
 64            f"frames={self.motion.num_frames()} "
 65            f"fps={self.motion.fps():.3f}"
 66        )
 67        self.check_error()
 68
 69    def pre_render(self):
 70        if self.editor.update(self.get_delta_time()):
 71            self._apply_motion_time()
 72
 73    def render(self):
 74        imgui.begin("BVH Character")
 75        imgui.text(Path(self.bvh_file).name)
 76        imgui.text(
 77            f"joints={self.motion.num_joints()} frames={self.motion.num_frames()} fps={self.motion.fps():.2f}"
 78        )
 79        changed, self.show_skeleton = imgui.checkbox(
 80            "show skeleton", self.show_skeleton
 81        )
 82        if changed:
 83            self.skeleton_visual.set_visible(self.show_skeleton)
 84        changed, self.show_joints = imgui.checkbox("show joints", self.show_joints)
 85        if changed:
 86            self.skeleton_visual.set_show_joints(self.show_joints)
 87        imgui.end()
 88
 89        if self.editor.render():
 90            self._apply_motion_time()
 91
 92    def _apply_motion_time(self):
 93        self.skeleton_visual.apply_motion(
 94            self.motion,
 95            self.editor.player.time,
 96            self.editor.player.loop,
 97        )
 98
 99
100def parse_args():
101    parser = argparse.ArgumentParser(description=__doc__)
102    parser.add_argument("bvh", nargs="?", type=Path, default=default_bvh_file())
103    parser.add_argument("--scale", type=float, default=1.0)
104    parser.add_argument("--width", type=int, default=1920)
105    parser.add_argument("--height", type=int, default=1080)
106    return parser.parse_args()
107
108
109def main():
110    args = parse_args()
111    app = BVHCharacterViewer(args.bvh, args.scale)
112    app.initialize(args.width, args.height, False, ke.UpAxis.Y)
113    app.start()
114
115
116if __name__ == "__main__":
117    main()

BVH skeleton and motion sequencer

Play multiple motion formats together

view_motion_party.py loads BVH, FBX, and AMASS/SMPL-X motion into the same scene. Each source becomes the common SkeletonMotion type, while its visual uses the appropriate skeleton or skinned-surface representation. The motion sequencer displays a separate duration bar for each clip and drives all three from one playback time.

python ./python/examples/view_motion_party.py \
  --bvh-file /path/to/motion.bvh \
  --fbx-file /path/to/character.fbx \
  --smpl-motion /path/to/amass_motion.npz

The SMPL-X neutral model is resolved from KangEngine’s repository model location. Use the command-line path options when the example assets or AMASS dataset are stored elsewhere.

Complete source: view_motion_party.py
  1"""Play BVH, FBX, and SMPL/AMASS motions together with sequencers."""
  2
  3from __future__ import annotations
  4
  5import argparse
  6from pathlib import Path
  7
  8import kangengine as ke
  9from kangengine import asset, imgui, visual
 10from kangengine.asset.smpl import SMPLXModel, repository_smplx_model_path
 11
 12
 13def repository_root() -> Path:
 14    return Path(__file__).resolve().parents[2]
 15
 16
 17def default_bvh_file() -> Path:
 18    return (
 19        repository_root() / "assets/external/SMPL_AMASS_T_HDM_bk_01-01_01_120_poses.bvh"
 20    )
 21
 22
 23def default_fbx_file() -> Path:
 24    return repository_root() / "assets/external/Capoeira (1).fbx"
 25
 26
 27def default_amass_file() -> Path:
 28    return Path.home() / "Dev/dataset/AMASS/TotalCapture/s3/walking2_poses.npz"
 29
 30
 31def _shift_root(state, offset: tuple[float, float, float]):
 32    root = state.root_translation()
 33    state.set_root_translation(
 34        [root.x + offset[0], root.y + offset[1], root.z + offset[2]]
 35    )
 36    return state
 37
 38
 39class MultiMotionViewer(ke.App):
 40    def __init__(
 41        self,
 42        bvh_file: Path,
 43        fbx_file: Path,
 44        smpl_motion_file: Path,
 45        fbx_clip_index: int,
 46        bvh_scale: float,
 47        fbx_scale: float,
 48        smpl_scale: float,
 49    ):
 50        super().__init__()
 51        self.bvh_file = bvh_file
 52        self.fbx_file = fbx_file
 53        self.smpl_motion_file = smpl_motion_file
 54        self.fbx_clip_index = fbx_clip_index
 55        self.bvh_scale = bvh_scale
 56        self.fbx_scale = fbx_scale
 57        self.smpl_scale = smpl_scale
 58
 59    def setup(self):
 60        self.show_bvh = True
 61        self.show_fbx = True
 62        self.show_smpl = True
 63
 64        materials = self.create_standard_materials()
 65        self.scene.add_ground(
 66            path="/ground",
 67            scale=30.0,
 68            material=materials.ground,
 69        )
 70        self.set_camera_view(
 71            position=[0.0, 1.8, 7.5],
 72            target=[0.0, 0.9, 0.0],
 73        )
 74
 75        self._setup_bvh(materials)
 76        self._setup_fbx()
 77        self._setup_smpl(materials)
 78
 79        self.motions = {
 80            "BVH": self.bvh_motion,
 81            "FBX": self.fbx_motion,
 82            "SMPL/AMASS": self.smpl_motion,
 83        }
 84        timeline_motion = max(
 85            self.motions.values(),
 86            key=lambda motion: motion.num_frames() / motion.fps(),
 87        )
 88        self.editor: ke.motion_module.MotionEditor = ke.motion_module.MotionEditor(
 89            motion=timeline_motion,
 90            motion_name="BVH + FBX + SMPL/AMASS",
 91        )
 92        self.editor.panel.set_motions(
 93            list(self.motions),
 94            [motion.num_frames() for motion in self.motions.values()],
 95            [motion.fps() for motion in self.motions.values()],
 96        )
 97        self.playback_controller.add_target(self.editor)
 98        self._apply_all_motion_times()
 99
100        for label, motion in self.motions.items():
101            print(
102                f"{label} loaded: frames={motion.num_frames()} "
103                f"joints={motion.num_joints()} fps={motion.fps():.3f}"
104            )
105        self.check_error()
106
107    def _setup_bvh(self, materials):
108        self.bvh_motion: ke.animation.SkeletonMotion = asset.BVHLoader.load_motion(
109            bvh_path=str(self.bvh_file),
110            scale=self.bvh_scale,
111        )
112        config: ke.visual.SkeletalVisualConfig = visual.SkeletalVisualConfig(
113            bone_color=ke.Vec4(0.35, 0.75, 1.0, 1.0),
114            joint_color=ke.Vec4(1.0, 0.55, 0.3, 1.0),
115            bone_radius=0.025,
116            joint_radius=0.035,
117            show_joints=True,
118        )
119        state: ke.animation.SkeletonState = _shift_root(
120            self.bvh_motion.sample(time=0.0, loop=True),
121            (-2.2, 0.0, 0.0),
122        )
123        self.bvh_visual: ke.visual.SkeletalVisual = visual.SkeletalVisual.define(
124            app=self,
125            material=materials.common,
126            path="/bvh_motion",
127            state=state,
128            config=config,
129        )
130
131    def _setup_fbx(self):
132        result: ke.asset.FBXImportResult = asset.FBXLoader.parse(
133            fbx_path=str(self.fbx_file),
134            clip_index=self.fbx_clip_index,
135            fps=-1.0,
136            scale=self.fbx_scale,
137        )
138        self.fbx_motion: ke.animation.SkeletonMotion = result.motion
139        self.fbx_surface: ke.visual.SkinnedSurface = (
140            visual.SkinnedSurface.create_from_fbx_result(
141                app=self,
142                path="/fbx_motion",
143                result=result,
144            )
145        )
146
147    def _setup_smpl(self, materials):
148        info: ke.asset.AMASSInfo = asset.AMASSLoader.inspect(
149            path=self.smpl_motion_file,
150        )
151        model: ke.asset.SMPLXModel = SMPLXModel.load(
152            path=repository_smplx_model_path(gender="neutral"),
153        )
154        self.smpl_body: ke.asset.SMPLXBody = model.create_body(betas=info.betas)
155        self.smpl_surface: ke.visual.SkinnedSurface = self.smpl_body.create_visual(
156            app=self,
157            path="/smpl_motion",
158            material=materials.pbr,
159            color=ke.Vec4(0.72, 0.82, 0.95, 1.0),
160        )
161        self.smpl_motion: ke.animation.SkeletonMotion = asset.AMASSLoader.load_motion(
162            path=self.smpl_motion_file,
163            skeleton_tree=self.smpl_body.skeleton_tree,
164            model_type="smplx",
165            up_axis=ke.UpAxis.Y,
166            scale=self.smpl_scale,
167        )
168
169    def _apply_bvh_time(self):
170        state = _shift_root(
171            self.bvh_motion.sample(
172                time=self.editor.player.time,
173                loop=self.editor.player.loop,
174            ),
175            (-2.5, 0.0, 0.0),
176        )
177        self.bvh_visual.apply_state(state=state)
178
179    def _apply_fbx_time(self):
180        state = _shift_root(
181            self.fbx_motion.sample(
182                time=self.editor.player.time,
183                loop=self.editor.player.loop,
184            ),
185            (0.0, 0.0, 0.0),
186        )
187        self.fbx_surface.apply_state(state=state)
188
189    def _apply_smpl_time(self):
190        state = self.smpl_motion.sample(
191            time=self.editor.player.time,
192            loop=self.editor.player.loop,
193        )
194        _shift_root(state, (2.5, 0.0, 0.0))
195        self.smpl_surface.apply_state(state=state)
196
197    def _apply_all_motion_times(self):
198        self._apply_bvh_time()
199        self._apply_fbx_time()
200        self._apply_smpl_time()
201
202    def pre_render(self):
203        if self.editor.update(dt=self.get_delta_time()):
204            self._apply_all_motion_times()
205
206    def render(self):
207        imgui.begin("BVH + FBX + SMPL Motion")
208        imgui.text("BVH skeleton    FBX skinned mesh    SMPL-X / AMASS")
209        changed, self.show_bvh = imgui.checkbox("show BVH", self.show_bvh)
210        if changed:
211            self.bvh_visual.set_visible(visible=self.show_bvh)
212        changed, self.show_fbx = imgui.checkbox("show FBX", self.show_fbx)
213        if changed:
214            self.fbx_surface.set_visible(visible=self.show_fbx)
215        changed, self.show_smpl = imgui.checkbox("show SMPL", self.show_smpl)
216        if changed:
217            self.smpl_surface.set_visible(visible=self.show_smpl)
218        if changed:
219            self._apply_smpl_time()
220        imgui.end()
221
222        if self.editor.render():
223            self._apply_all_motion_times()
224
225
226def parse_args():
227    parser = argparse.ArgumentParser(description=__doc__)
228    parser.add_argument("--bvh-file", type=Path, default=default_bvh_file())
229    parser.add_argument("--fbx-file", type=Path, default=default_fbx_file())
230    parser.add_argument("--smpl-motion", type=Path, default=default_amass_file())
231    parser.add_argument("--fbx-clip-index", type=int, default=0)
232    parser.add_argument("--bvh-scale", type=float, default=1.0)
233    parser.add_argument("--fbx-scale", type=float, default=0.01)
234    parser.add_argument("--smpl-scale", type=float, default=1.0)
235    parser.add_argument("--width", type=int, default=1920)
236    parser.add_argument("--height", type=int, default=1080)
237    return parser.parse_args()
238
239
240def main():
241    args = parse_args()
242    paths = {
243        "BVH": args.bvh_file.expanduser().resolve(),
244        "FBX": args.fbx_file.expanduser().resolve(),
245        "SMPL motion": args.smpl_motion.expanduser().resolve(),
246    }
247    for label, path in paths.items():
248        if not path.exists():
249            raise FileNotFoundError(f"{label} file not found: {path}")
250
251    app = MultiMotionViewer(
252        paths["BVH"],
253        paths["FBX"],
254        paths["SMPL motion"],
255        args.fbx_clip_index,
256        args.bvh_scale,
257        args.fbx_scale,
258        args.smpl_scale,
259    )
260    app.initialize(
261        width=args.width,
262        height=args.height,
263        hide_ui=False,
264        up_axis=ke.UpAxis.Y,
265    )
266    app.start()
267
268
269if __name__ == "__main__":
270    main()

BVH, FBX, and SMPL-X motion playing together

Other examples:

  • python/examples/view_fbx_character.py

  • python/examples/view_fbx_character2.py

  • python/examples/view_fbx_character_apply_pose.py

  • python/examples/view_smpl_motion.py

  • python/examples/view_motion.py