Articulations

MJCF, URDF, and USD loaders produce a common ArticulationDesc. Load a robot, then pass its data to world.add_articulation().

Load a robot

Choose the loader for your file:

# world is an existing KangSimWorld.
data: ke.asset.ArticulationDesc = world.load_mjcf("robot.xml", order="DFS")
# Or:
data = world.load_urdf("robot.urdf", order="DFS")
data = world.load_usd("robot.usd", order="DFS")

USD requires a native build with USE_USD enabled. It selects the default prim; pass prim_path="/Robot" to select a different robot subtree.

Format

Supported joints

3-axis rotation

MJCF

Fixed, Revolute (hinge)

3 orthogonal Revolute joints on one body

URDF

Fixed, Revolute, Prismatic

3 Revolute joints with intermediate links

USD

Fixed, Revolute, Prismatic

3 Revolute joints with intermediate links

Native MJCF ball/slide and USD Spherical joint import are not supported.

Create the articulation

config = ke.physics.ArticulationConfig.free_base()

record = world.add_articulation(
    data,
    env_id=0,
    obj_id=0,
    name="robot",
    config=config,
)
robot = record.articulation

For all three formats, choose ArticulationConfig.free_base() for a floating root or ArticulationConfig.fixed_base() for a fixed root.

The following visualization example uses an MJCF asset:

Use the same order for loading and visualization.

robot_visual = visual.add_articulation_scene_graph(
    0,
    0,
    mjcf_path,
    path="/robot",
    order="DFS",
    material=robot_material,
)

Control example

Run the complete control example with an MJCF file:

python ./python/examples/mjcf_dof_control.py /path/to/robot.xml
Complete source: mjcf_dof_control.py
  1"""Generic MJCF articulation DOF position-control viewer."""
  2
  3from __future__ import annotations
  4
  5import argparse
  6import math
  7from pathlib import Path
  8
  9import numpy as np
 10
 11import kangengine as ke
 12from kangengine import imgui, keys
 13
 14
 15def package_asset_path(*parts: str) -> str:
 16    return str(Path(ke.__file__).resolve().parent / "assets" / Path(*parts))
 17
 18
 19def default_mjcf_path() -> Path:
 20    return Path(package_asset_path("characters", "kw", "kw.xml"))
 21
 22
 23class MjcfDofControlApp(ke.App):
 24    """Load an MJCF articulation and expose every DOF as an ImGui slider."""
 25
 26    window_title = "MJCF DOF Control"
 27    object_name = "mjcf"
 28    prim_base_path = "/mjcf"
 29    camera_pos = (3.8, -5.4, 1.2)
 30    camera_target = (0.0, 0.0, 0.45)
 31    # None preserves per-geom MJCF rgba. Set an RGBA tuple to force a single
 32    # override color for the whole articulation.
 33    visual_color = None  # np.array([1,1,1, 1.0])
 34    ground_size = 10.0
 35    root_pos = (0.0, 0.0, 1.5)
 36    root_rot_xyzw = (0.0, 0.0, 0.0, 1.0)
 37    fixed_base = False
 38    order = "DFS"
 39    sim_dt = 1.0 / 240.0
 40    step_substeps = 4
 41    default_kp = 120.0
 42    default_kd = 12.0
 43    default_anim_amp = 0.35
 44    default_anim_speed = 1.0
 45    default_contact_force_scale = 0.002
 46    contact_force_threshold = 1e-3
 47    visual_alpha_with_collision = 0.1
 48    drag_force_debug_path = "/debug/mjcf_drag_force"
 49    drag_force_target_debug_path = "/debug/mjcf_drag_force_target"
 50
 51    def __init__(self, mjcf_path: str | Path):
 52        super().__init__()
 53        self.mjcf_path = str(Path(mjcf_path).expanduser().resolve())
 54
 55    def setup(self):
 56        self.timing = self.configure_timing(
 57            ke.SimulationTimingConfig.from_dt(
 58                physics_dt=self.sim_dt,
 59                fixed_dt=self.sim_dt * self.step_substeps,
 60                render_hz=60.0,
 61            )
 62        )
 63        self.set_simulation_hotkeys_enabled(True)
 64        self.elapsed = 0.0
 65        self.kp = float(self.default_kp)
 66        self.kd = float(self.default_kd)
 67        self.animate = True
 68        self.anim_amp = float(self.default_anim_amp)
 69        self.anim_speed = float(self.default_anim_speed)
 70        self.show_collision = False
 71        self.show_contact_forces = False
 72        self.contact_force_scale = float(self.default_contact_force_scale)
 73        self.contact_force_view = None
 74        self.contact_force_color = np.array([[1.0, 0.25, 0.05, 1.0]], dtype=np.float32)
 75        self.empty_vec3 = np.empty((0, 3), dtype=np.float32)
 76        self.empty_vec4 = np.empty((0, 4), dtype=np.float32)
 77        red = ke.ColorLibrary.get(ke.ColorType.RED)
 78        magenta = ke.ColorLibrary.get(ke.ColorType.MAGENTA)
 79        self.drag_force_line_color = np.array(
 80            [[red.r, red.g, red.b, red.a]], dtype=np.float32
 81        )
 82        self.drag_force_target_color = np.array(
 83            [[magenta.r, magenta.g, magenta.b, magenta.a]], dtype=np.float32
 84        )
 85        self.drag_force_line_starts = np.empty((3, 3), dtype=np.float32)
 86        self.drag_force_line_ends = np.empty((3, 3), dtype=np.float32)
 87        self.drag_force_up_z = np.array([0.0, 0.0, 1.0], dtype=np.float32)
 88        self.drag_force_up_y = np.array([0.0, 1.0, 0.0], dtype=np.float32)
 89        self.drag_force_enabled = True
 90        self.drag_force_stiffness = 750.0
 91        self.drag_force_damping = 75.0
 92        self.drag_force_max = 300.0
 93        self.drag_force_arrow_scale = 0.003
 94        self.show_drag_force_arrow = True
 95        self._drag_force_body_id = None
 96        self._drag_force_local_anchor = None
 97        self._drag_force_target = None
 98        self._drag_force_anchor_world = None
 99        self._drag_force_vector = None
100        self._clear_drag_force_arrow()
101
102        self.configure_camera()
103        self.standard_materials = self.create_standard_materials()
104        self.debug_material = self.standard_materials.common
105        self.create_world()
106        self.load_articulation()
107        self._reset()
108        self.print_summary()
109
110    def configure_camera(self):
111        self.get_camera().set_camera_pos(ke.Vec3(*self.camera_pos))
112        self.get_camera().set_target_pos(ke.Vec3(*self.camera_target))
113
114    def create_world(self):
115        self.world = ke.sim.KangSimWorld(
116            num_envs=1,
117            sim_dt=self.timing.physics_dt,
118            add_ground=True,
119        )
120        self.visual = ke.visual.sim.SimWorldVisualizer(self, self.world)
121
122        self.ground_view = self.scene.add_ground(
123            "/ground",
124            scale=float(self.ground_size),
125            material=self.standard_materials.ground,
126        )
127
128    def load_articulation(self):
129        data = self.world.load_mjcf(self.mjcf_path, order=self.order)
130        self.obj_id = 0
131        config = (
132            ke.physics.ArticulationConfig.fixed_base()
133            if self.fixed_base
134            else ke.physics.ArticulationConfig.free_base()
135        )
136        self.robot = self.world.add_articulation(
137            data,
138            env_id=0,
139            obj_id=self.obj_id,
140            name=self.object_name,
141            config=config,
142        ).articulation
143
144        self.articulation_visual_view = self.visual.add_articulation_scene_graph(
145            0,
146            self.obj_id,
147            self.mjcf_path,
148            path=self.prim_base_path,
149            order=self.order,
150            material=self.standard_materials.pbr,
151            collision_path=f"{self.prim_base_path}_collision",
152            show_collision=self.show_collision,
153            color=(
154                None
155                if self.visual_color is None
156                else np.array(self.visual_color, dtype=np.float32)
157            ),
158        )
159        self.visual_body_prims = self.articulation_visual_view.prims
160        # self.collision_body_prims = self.articulation_visual_view.collision_visuals
161
162        self.num_dofs = self.robot.num_dofs()
163        self.dof_names = self.world.state.get_obj_dof_names(self.obj_id)
164        self.dof_limits = np.asarray(
165            self.world.state.get_obj_dof_limits(self.obj_id), dtype=np.float32
166        )
167        if self.dof_limits.shape != (self.num_dofs, 2):
168            self.dof_limits = np.tile(
169                np.array([-math.pi, math.pi], dtype=np.float32),
170                (self.num_dofs, 1),
171            )
172        self.targets = self.initial_targets()
173
174    def initial_targets(self) -> np.ndarray:
175        return np.zeros(self.num_dofs, dtype=np.float32)
176
177    def print_summary(self):
178        print(
179            f"{self.object_name} loaded: links={self.robot.num_links()} dofs={self.num_dofs}"
180        )
181        print("DOFs:", ", ".join(self.dof_names))
182        print("XML:", self.mjcf_path)
183
184    def _reset(self):
185        self.elapsed = 0.0
186        self.targets[:] = self.initial_targets()
187        self.world.set_root_state(
188            None,
189            self.obj_id,
190            np.array(self.root_pos, dtype=np.float32),
191            np.array(self.root_rot_xyzw, dtype=np.float32),
192        )
193        self.world.set_dof_state(None, self.obj_id, self.targets)
194        self.world.set_cmd(
195            None,
196            self.obj_id,
197            self.targets,
198            mode=ke.sim.ControlMode.POS,
199            kp=self.kp,
200            kd=self.kd,
201        )
202        self.world.step(substeps=0, apply_commands=False)
203        self.visual.sync()
204        self._update_contact_force_arrows()
205        self._clear_drag_force()
206
207    def _animated_targets(self):
208        out = np.zeros_like(self.targets)
209        for i in range(self.num_dofs):
210            lo, hi = self.slider_limits(i)
211            span = max(0.0, min(float(hi - lo) * 0.35, self.anim_amp))
212            center = 0.5 * float(lo + hi)
213            phase = self.elapsed * self.anim_speed + i * 0.75
214            out[i] = np.clip(center + span * math.sin(phase), lo, hi)
215        return out
216
217    def slider_limits(self, dof_index: int) -> tuple[float, float]:
218        lo, hi = self.dof_limits[dof_index]
219        if not np.isfinite(lo) or not np.isfinite(hi) or hi <= lo:
220            return -math.pi, math.pi
221        return float(lo), float(hi)
222
223    def pre_update(self):
224        if self.was_key_pressed(keys.R):
225            self._reset()
226        if self.was_key_pressed(keys.Q):
227            self.animate = not self.animate
228
229    def fixed_update(self, fixed_dt):
230        self.elapsed += fixed_dt
231        if self.animate:
232            self.targets[:] = self._animated_targets()
233
234        self.world.set_cmd(
235            None,
236            self.obj_id,
237            self.targets,
238            mode=ke.sim.ControlMode.POS,
239            kp=self.kp,
240            kd=self.kd,
241        )
242        self.world.advance(fixed_dt)
243
244    def pre_render(self):
245        self.visual.sync()
246        self._update_contact_force_arrows()
247        self._update_drag_force_arrow()
248        self.check_error()
249
250    def on_force_drag_begin(self, result, target):
251        if not self.drag_force_enabled or not result.hit:
252            self._clear_drag_force()
253            return
254
255        pick = self.articulation_visual_view.pick_body(result)
256        if pick is None:
257            self._clear_drag_force()
258            return
259
260        self._drag_force_body_id = int(pick.body_id)
261        body_state = self._drag_body_state(self._drag_force_body_id)
262        if body_state is None:
263            return
264        body_pos, body_rot, _, _ = body_state
265        hit_pos = self._vec3_to_np(result.position)
266        self._drag_force_local_anchor = self._quat_inverse_rotate_xyzw(
267            body_rot, hit_pos - body_pos
268        )
269        self._drag_force_target = self._vec3_to_np(target)
270        self._apply_drag_force()
271        self._update_drag_force_arrow()
272
273    def on_force_drag_update(self, result, target):
274        if not self.drag_force_enabled or self._drag_force_body_id is None:
275            self._clear_drag_force()
276            return
277        self._drag_force_target = self._vec3_to_np(target)
278        self._apply_drag_force()
279        self._update_drag_force_arrow()
280
281    def on_force_drag_end(self):
282        self._clear_drag_force()
283
284    def _apply_drag_force(self):
285        if (
286            self._drag_force_body_id is None
287            or self._drag_force_local_anchor is None
288            or self._drag_force_target is None
289        ):
290            return
291        body_state = self._drag_body_state(self._drag_force_body_id)
292        if body_state is None:
293            return
294        body_pos, body_rot, body_vel, body_ang_vel = body_state
295        radius = self._quat_rotate_xyzw(body_rot, self._drag_force_local_anchor)
296        anchor_world = body_pos + radius
297        point_vel = body_vel + np.cross(body_ang_vel, radius)
298        force = (self._drag_force_target - anchor_world) * float(
299            self.drag_force_stiffness
300        ) - point_vel * float(self.drag_force_damping)
301        norm = float(np.linalg.norm(force))
302        if norm > float(self.drag_force_max) > 0.0:
303            force *= float(self.drag_force_max) / norm
304        self._drag_force_anchor_world = anchor_world.astype(np.float32)
305        self._drag_force_vector = force.astype(np.float32)
306        self.world.set_body_force_at_position(
307            0,
308            self.obj_id,
309            self._drag_force_body_id,
310            force.astype(np.float32),
311            anchor_world.astype(np.float32),
312        )
313
314    def _clear_drag_force_arrow(self):
315        self.clear_debug_lines(self.drag_force_debug_path)
316        self.clear_debug_points(self.drag_force_target_debug_path)
317
318    def _update_drag_force_arrow(self):
319        if (
320            not self.show_drag_force_arrow
321            or self._drag_force_anchor_world is None
322            or self._drag_force_vector is None
323            or self._drag_force_target is None
324        ):
325            self._clear_drag_force_arrow()
326            return
327
328        start = self._drag_force_anchor_world
329        force = self._drag_force_vector
330        force_len = float(np.linalg.norm(force))
331        if force_len < 1e-5:
332            self._clear_drag_force_arrow()
333            return
334
335        direction = force / force_len
336        end = start + force * float(self.drag_force_arrow_scale)
337        shaft_len = float(np.linalg.norm(end - start))
338        head_len = min(max(shaft_len * 0.25, 0.04), 0.18)
339
340        side = np.cross(direction, self.drag_force_up_z)
341        if np.linalg.norm(side) < 1e-5:
342            side = np.cross(direction, self.drag_force_up_y)
343        side = side / max(float(np.linalg.norm(side)), 1e-8)
344
345        back = end - direction * head_len
346        left = back + side * head_len * 0.45
347        right = back - side * head_len * 0.45
348        self.drag_force_line_starts[0] = start
349        self.drag_force_line_starts[1] = end
350        self.drag_force_line_starts[2] = end
351        self.drag_force_line_ends[0] = end
352        self.drag_force_line_ends[1] = left
353        self.drag_force_line_ends[2] = right
354        self.log_debug_lines(
355            self.drag_force_debug_path,
356            self.drag_force_line_starts,
357            self.drag_force_line_ends,
358            self.drag_force_line_color,
359            3.0,
360        )
361        self.log_debug_points(
362            self.drag_force_target_debug_path,
363            self._drag_force_target.reshape(1, 3),
364            self.drag_force_target_color,
365            10.0,
366        )
367
368    def _drag_body_state(self, body_id: int):
369        body_pos = self._state_array(self.world.state.get_body_pos(self.obj_id)[0])
370        if body_id >= body_pos.shape[0]:
371            self._clear_drag_force()
372            return None
373        body_rot = self._state_array(self.world.state.get_body_rot(self.obj_id)[0])
374        body_vel = self._state_array(self.world.state.get_body_vel(self.obj_id)[0])
375        body_ang_vel = self._state_array(
376            self.world.state.get_body_ang_vel(self.obj_id)[0]
377        )
378        return (
379            body_pos[body_id],
380            body_rot[body_id],
381            body_vel[body_id],
382            body_ang_vel[body_id],
383        )
384
385    def _clear_drag_force(self):
386        if getattr(self, "_drag_force_body_id", None) is not None:
387            self.world.set_body_force(
388                0,
389                self.obj_id,
390                int(self._drag_force_body_id),
391                np.zeros(3, dtype=np.float32),
392            )
393        self._drag_force_body_id = None
394        self._drag_force_local_anchor = None
395        self._drag_force_target = None
396
397    def _clear_contact_force_arrows(self):
398        if self.contact_force_view is None:
399            return
400        self.contact_force_view.update_arrows(
401            self.empty_vec3,
402            self.empty_vec3,
403            self.empty_vec4,
404        )
405
406    def _update_contact_force_arrows(self):
407        if not self.show_contact_forces:
408            self._clear_contact_force_arrows()
409            return
410
411        # Body-aggregated force visualization.
412        # the arrows start at link origins instead of real contact points.
413        # body_pos = np.asarray(
414        #     self.world.state.get_body_pos(self.obj_id)[0], dtype=np.float32
415        # )
416        # forces = np.asarray(
417        #     self.world.state.get_contact_forces(self.obj_id)[0], dtype=np.float32
418        # )
419        # active = np.linalg.norm(forces, axis=1) > float(self.contact_force_threshold)
420        # starts = body_pos[active]
421        # ends = starts + forces[active] * float(self.contact_force_scale)
422
423        contacts = self.world.physics.get_contacts()
424        starts = []
425        ends = []
426        dt = max(float(self.world.sim_dt), 1e-8)
427        for contact in contacts:
428            position = self._vec3_to_np(contact.position)
429            force = self._vec3_to_np(contact.impulse) / dt
430            if np.linalg.norm(force) <= float(self.contact_force_threshold):
431                continue
432            starts.append(position)
433            ends.append(position + force * float(self.contact_force_scale))
434
435        if not starts:
436            self._clear_contact_force_arrows()
437            return
438
439        starts = np.asarray(starts, dtype=np.float32)
440        ends = np.asarray(ends, dtype=np.float32)
441        colors = np.repeat(self.contact_force_color, starts.shape[0], axis=0)
442
443        if self.contact_force_view is None:
444            self.contact_force_view = self.scene.log_arrows(
445                "/debug/contact_forces",
446                self.debug_material,
447                starts,
448                ends,
449                colors,
450                0.015,
451                12,
452            )
453        else:
454            self.contact_force_view.update_arrows(starts, ends, colors)
455
456    @staticmethod
457    def _vec3_to_np(value) -> np.ndarray:
458        return np.array(
459            [float(value.x), float(value.y), float(value.z)], dtype=np.float32
460        )
461
462    @staticmethod
463    def _state_array(value) -> np.ndarray:
464        if hasattr(value, "detach"):
465            value = value.detach().cpu().numpy()
466        return np.asarray(value, dtype=np.float32)
467
468    @staticmethod
469    def _quat_rotate_xyzw(quat, vector) -> np.ndarray:
470        q = np.asarray(quat, dtype=np.float32)
471        v = np.asarray(vector, dtype=np.float32)
472        qv = q[:3]
473        t = 2.0 * np.cross(qv, v)
474        return (v + q[3] * t + np.cross(qv, t)).astype(np.float32)
475
476    @classmethod
477    def _quat_inverse_rotate_xyzw(cls, quat, vector) -> np.ndarray:
478        q = np.asarray(quat, dtype=np.float32).copy()
479        q[:3] *= -1.0
480        return cls._quat_rotate_xyzw(q, vector)
481
482    def _set_visual_alpha(self, alpha: float):
483        if self.visual_color is None:
484            self.articulation_visual_view.set_alpha(alpha)
485            return
486        color = np.array(self.visual_color, dtype=np.float32).reshape(-1)
487        if color.size == 3:
488            color = np.concatenate([color, np.ones(1, dtype=np.float32)])
489        color = color[:4].copy()
490        color[3] = float(alpha)
491        self.articulation_visual_view.set_color(color)
492
493    def _set_collision_visible(self, visible: bool):
494        self.show_collision = bool(visible)
495        self.articulation_visual_view.set_collision_visible(self.show_collision)
496        self._set_visual_alpha(
497            self.visual_alpha_with_collision if self.show_collision else 1.0
498        )
499
500    def render(self):
501        imgui.begin(self.window_title)
502        state = "paused" if self.is_simulation_paused() else "running"
503        imgui.text(f"State: {state}")
504        imgui.text(
505            "Enter: play/pause    Space: pause/step    R: reset    Q: auto motion"
506        )
507        imgui.text(f"Links: {self.robot.num_links()}  DOFs: {self.num_dofs}")
508        imgui.text(Path(self.mjcf_path).name)
509        imgui.separator()
510        _, self.kp = imgui.slider_float("kp", self.kp, 0.0, 1000.0)
511        _, self.kd = imgui.slider_float("kd", self.kd, 0.0, 80.0)
512        _, self.animate = imgui.checkbox("Animate targets", self.animate)
513        _, self.anim_amp = imgui.slider_float("anim amplitude", self.anim_amp, 0.0, 1.5)
514        _, self.anim_speed = imgui.slider_float("anim speed", self.anim_speed, 0.0, 6.0)
515        changed, self.show_collision = imgui.checkbox(
516            "Show collision prims",
517            self.show_collision,
518        )
519        if changed:
520            self._set_collision_visible(self.show_collision)
521        changed, self.show_contact_forces = imgui.checkbox(
522            "Show contact forces",
523            self.show_contact_forces,
524        )
525        if changed:
526            self._update_contact_force_arrows()
527        _, self.contact_force_scale = imgui.slider_float(
528            "contact force scale",
529            self.contact_force_scale,
530            0.0,
531            0.02,
532        )
533        imgui.separator()
534        changed, self.drag_force_enabled = imgui.checkbox(
535            "Enable drag force",
536            self.drag_force_enabled,
537        )
538        if changed and not self.drag_force_enabled:
539            self._clear_drag_force()
540        changed, self.show_drag_force_arrow = imgui.checkbox(
541            "Show drag force arrow",
542            self.show_drag_force_arrow,
543        )
544        if changed and not self.show_drag_force_arrow:
545            self._clear_drag_force_arrow()
546        _, self.drag_force_stiffness = imgui.slider_float(
547            "drag force stiffness",
548            self.drag_force_stiffness,
549            0.0,
550            1000.0,
551        )
552        _, self.drag_force_damping = imgui.slider_float(
553            "drag force damping",
554            self.drag_force_damping,
555            0.0,
556            80.0,
557        )
558        _, self.drag_force_max = imgui.slider_float(
559            "drag force max",
560            self.drag_force_max,
561            0.0,
562            2000.0,
563        )
564        _, self.drag_force_arrow_scale = imgui.slider_float(
565            "drag force arrow scale",
566            self.drag_force_arrow_scale,
567            0.0005,
568            0.02,
569        )
570        if self._drag_force_body_id is not None:
571            imgui.text(f"Dragging body: {self._drag_force_body_id}")
572        imgui.separator()
573
574        for i, name in enumerate(self.dof_names):
575            lo, hi = self.slider_limits(i)
576            changed, value = imgui.slider_float(name, float(self.targets[i]), lo, hi)
577            if changed:
578                self.targets[i] = value
579                self.animate = False
580
581        pos = self.world.state.get_dof_pos(self.obj_id)[0]
582        imgui.separator()
583        imgui.text("Current DOF positions")
584        for name, value in zip(self.dof_names, pos):
585            imgui.text(f"{name}: {float(value): .3f}")
586        imgui.end()
587
588    def cleanup(self):
589        if hasattr(self, "visual"):
590            self.visual.release()
591        if hasattr(self, "world"):
592            self.world.release()
593
594
595def main():
596    parser = argparse.ArgumentParser()
597    parser.add_argument(
598        "mjcf_path",
599        nargs="?",
600        default=str(default_mjcf_path()),
601        help="Path to an MJCF XML file",
602    )
603    parser.add_argument(
604        "--fixed-base",
605        action="store_true",
606        help="Use a fixed root instead of the default free root.",
607    )
608    parser.add_argument("--order", default="DFS", choices=("DFS", "BFS"))
609    parser.add_argument("--width", type=int, default=1920)
610    parser.add_argument("--height", type=int, default=1080)
611    args = parser.parse_args()
612
613    class CliMjcfDofControlApp(MjcfDofControlApp):
614        fixed_base = args.fixed_base
615        order = args.order
616
617    app = CliMjcfDofControlApp(args.mjcf_path)
618    app.initialize(args.width, args.height, False, ke.UpAxis.Z)
619    app.start()
620
621
622if __name__ == "__main__":
623    main()

MJCF articulation

Collision debug

Loaded MJCF articulation

MJCF collision debug geometry

USD import requirements

Prepare a USD robot with:

  • Z-up, metersPerUnit = 1, and kilogramsPerUnit = 1.

  • One connected body tree with explicit positive mass and inertia.

  • convexHull collision meshes and no joint connecting the root to the world.

Configure initial states, drives, simulation settings, and materials after loading. These scene settings are not fully reproduced by the importer. Unsupported joint or collider configurations raise errors; inspect warnings for omitted properties:

result: ke.asset.USDArticulationImportResult = ke.asset.USDLoader.parse_articulation(
    usd_path="robot.usd", prim_path="/Robot"
)
print(result.diagnostics.warnings)