Files
2026-07-13 13:05:14 +08:00

141 lines
5.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""
An example of how to load and animate a URDF given some changing joint angles.
Usage:
python -m animated_urdf
"""
from __future__ import annotations
import argparse
import math
from pathlib import Path
import rerun as rr
import rerun.blueprint as rrb
TIMELINE = "example_time"
def main() -> None:
parser = argparse.ArgumentParser(
description="An example of how to load and animate a URDF given some changing joint angles.",
)
rr.script_add_args(parser)
parser.add_argument(
"--dual",
action="store_true",
help="Load the same URDF twice with different frame prefixes (dual-arm demo).",
)
args = parser.parse_args()
urdf_path = Path(__file__).parent.parent.parent / "rust" / "animated_urdf" / "data" / "so100.urdf"
if args.dual:
run_dual(args, urdf_path)
else:
run_single(args, urdf_path)
def run_single(args: argparse.Namespace, urdf_path: Path) -> None:
duration = 0.0
rec = rr.script_setup(args, "rerun_example_animated_urdf")
rec.set_time(TIMELINE, duration=duration)
# Log the URDF file once
rec.log_file_from_path(urdf_path)
# Load the URDF tree structure into memory
urdf_tree = rr.urdf.UrdfTree.from_file_path(urdf_path)
# Hide the collision geometries by default in the viewer.
blueprint = rrb.Grid(
rrb.Spatial3DView(
name="Animated URDF", overrides={"so_arm100/collision_geometries": rrb.EntityBehavior(visible=False)}
)
)
rec.send_blueprint(blueprint)
for step in range(10000):
for joint_index, joint in enumerate(urdf_tree.joints()):
if joint.joint_type == "revolute":
# Usually this angle would come from a measurement, here we just fake something
dynamic_angle = _fake_angle(joint, step, joint_index, phase=0.0)
# Rerun loads the URDF transforms with child/parent frame relations.
# To move a joint, we just need to log a new transform between those frames.
# Here, we use the `compute_transform` method that automatically takes care
# of setting the frame names and calculating the full transform from the joint angle.
transform = joint.compute_transform(dynamic_angle, clamp=True)
rec.log("transforms", transform)
# We can also work with links from the URDF tree.
# Here, we change the color of the visual mesh entities of the "jaw" link based on the joint angle.
link = urdf_tree.get_joint_child(joint)
if link.name == "jaw":
for visual_path in urdf_tree.get_visual_geometry_paths(link):
normalized_angle = (dynamic_angle - joint.limit_lower) / (joint.limit_upper - joint.limit_lower)
rgba = [1.0 - normalized_angle, normalized_angle, 0, 0.5]
rec.log(visual_path, rr.Asset3D.from_fields(albedo_factor=rgba))
duration += 0.03
rec.set_time(TIMELINE, duration=duration)
rr.script_teardown(args)
def run_dual(args: argparse.Namespace, urdf_path: Path) -> None:
"""Load the same URDF twice with different frame prefixes (dual-arm demo)."""
rec = rr.script_setup(args, "rerun_example_animated_urdf")
# Load the same URDF twice with different prefixes.
# - entity_path_prefix separates geometry in the entity tree
# - frame_prefix makes frame IDs unique ("left/base", "right/base", …)
left = rr.urdf.UrdfTree.from_file_path(urdf_path, entity_path_prefix="left", frame_prefix="left/")
right = rr.urdf.UrdfTree.from_file_path(urdf_path, entity_path_prefix="right", frame_prefix="right/")
# Log both robots (geometry + static transforms with prefixed frame IDs).
left.log_urdf_to_recording()
right.log_urdf_to_recording()
# Offset the arms so they don't overlap.
rec.log("left", rr.Transform3D(translation=[-0.2, 0, 0]), static=True)
rec.log("right", rr.Transform3D(translation=[0.2, 0, 0]), static=True)
blueprint = rrb.Grid(
rrb.Spatial3DView(
name="Dual Arm",
overrides={
"left/so_arm100/collision_geometries": rrb.EntityBehavior(visible=False),
"right/so_arm100/collision_geometries": rrb.EntityBehavior(visible=False),
},
)
)
rec.send_blueprint(blueprint)
for step in range(10000):
rec.set_time("step", sequence=step)
for joint_index, joint in enumerate(left.joints()):
if joint.joint_type == "revolute":
angle = _fake_angle(joint, step, joint_index, phase=0.0)
rec.log("left/joint_transforms", joint.compute_transform(angle, clamp=True))
for joint_index, joint in enumerate(right.joints()):
if joint.joint_type == "revolute":
angle = _fake_angle(joint, step, joint_index, phase=2.0)
rec.log("right/joint_transforms", joint.compute_transform(angle, clamp=True))
rr.script_teardown(args)
def _fake_angle(joint: rr.urdf.UrdfJoint, step: int, joint_index: int, phase: float) -> float:
"""Generate a smooth oscillating angle within the joint's limits."""
sin_value = math.sin(step * (0.02 + joint_index / 100.0) + phase)
return joint.limit_lower + (sin_value + 1.0) / 3.0 * (joint.limit_upper - joint.limit_lower)
if __name__ == "__main__":
main()