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

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

Other examples:
python/examples/view_fbx_character.pypython/examples/view_fbx_character2.pypython/examples/view_fbx_character_apply_pose.pypython/examples/view_smpl_motion.pypython/examples/view_motion.py