Skip to content

feat(mujoco): implement off-screen rendering and generic actuator ctrl#43

Draft
shubhambaid wants to merge 3 commits intodora-rs:mainfrom
shubhambaid:feat/mujoco-offscreen-rendering
Draft

feat(mujoco): implement off-screen rendering and generic actuator ctrl#43
shubhambaid wants to merge 3 commits intodora-rs:mainfrom
shubhambaid:feat/mujoco-offscreen-rendering

Conversation

@shubhambaid
Copy link

This PR upgrades the mujoco-client from a passive viewer to a fully functional, generic simulation node capable of handling both vision and physics-based control.

Key Changes

  1. Off-Screen Rendering: Replaced the deprecated mujoco.Renderer with mujoco.GLContext and mujoco.MjrContext. The node now dynamically discovers all elements in the loaded XML and publishes high-fidelity JPEG arrays for each.

  2. Physics-Accurate Actuation: Switched the action input handler to write directly to data.ctrl rather than teleporting joints via data.qpos. This ensures that all actuator limits, gear ratios, and damping parameters defined in the user's MJCF are properly respected by the physics engine.

  3. Decoupled Event Loops: Separated the high-frequency physics step (tick) from the lower-frequency vision step (render). This prevents high-res image encoding from bottlenecking the control loop.

  4. Dynamic Configuration: Removed legacy JSON configuration and hardcoded joint assumptions. The node now fully configures its inputs and outputs dynamically based purely on the provided --scene XML.

Copy link
Contributor

@kou kou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we have 4 key changes, how about splitting this PR to 4 PRs?

Comment on lines 45 to 49
print(
f"Model loaded: nq={self.m.nq} nv={self.m.nv} "
f"nu={self.m.nu} ncam={self.m.ncam}",
flush=True,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We remove this debug print when this is ready, right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! I left those in during my local testing to ensure the dynamic camera discovery was actually working. I have removed the prints and the warning, so the node will now run silently if no cameras are present.

Comment on lines 27 to 34
def _cam_topic(cam_name: str) -> str:
"""Derive a Dora-safe output topic name from a camera's XML name.
self.node = Node(config["name"])
Hyphens are replaced with underscores so the name is valid as a YAML
key without quoting. No other transformation is applied, keeping the
mapping transparent: "camera-wrist-right" → "camera_wrist_right".
"""
return cam_name.replace("-", "_")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this?

I think that camera-wrist-right is also a valid YAML key. It doesn't need quoting. For example, this is a valid YAML:

camera-wrist-right: value

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally converted them to underscores because some older data serialization libraries occasionally complain about hyphens in dictionary keys. But since standard YAML handles it perfectly fine and it matches the XML exactly, I completely agree. I've removed the conversion logic!

Comment on lines 15 to 18
#Image constants (must match the downstream pipeline expectations)
_IMG_W: int = 960
_IMG_H: int = 600
_JPEG_QUALITY: int = 90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make them cusotmizable by command line arguments and/or environment variables?

Comment on lines 77 to 78
if not self._cameras:
print("WARNING: no cameras found in model — no image output", flush=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make image output optional (no warning)?

Comment on lines 193 to 195
parser = argparse.ArgumentParser(description="Generic MuJoCo simulation node")
parser.add_argument("--name", type=str, default="mujoco")
parser.add_argument("--scene", type=str, help="Path to the MuJoCo XML scene file")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you change them instead of using the current code?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the node no longer explicitly requires the --config JSON to map joint names (as it dynamically uses data.ctrl), I had cleaned up the arguments to reflect only what it currently uses. However, I want to avoid unnecessary diff noise, so I have restored your original argparse boilerplate and just appended the new image arguments.

continue

scene = os.getenv("SCENE", args.scene)
eid = event["id"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you rename event_id to eid? I feel that event_id is more readable than eid.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I abbreviated it to eid simply to keep the line lengths a bit shorter inside the nested event loop.

elif eid == "action":
self._handle_action(event["value"])

def _handle_action(self, value: pa.Array | pa.ChunkedArray) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that event["value"] must be pa.Array.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added those explicitly to catch silent NumPy broadcasting errors just in case a mismatched array slipped through from an upstream node. However, since dora-rs already strictly enforces these schemas at the routing level, doing it again here is definitely redundant. I've stripped the checks out to keep the fast path clean.

Comment on lines 127 to 132
if not isinstance(value, (pa.Array, pa.ChunkedArray)):
print(
f"WARNING: action expected pa.Array, got {type(value).__name__} — ignored",
flush=True,
)
return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this check? I think that value must be pa.Array.

@shubhambaid
Copy link
Author

If we have 4 key changes, how about splitting this PR to 4 PRs?

Since the physics and camera changes are tightly coupled to make the simulation drop-in ready for our inference graph, I'd love to keep this in a single PR, but I will gladly strip out all the unnecessary formatting and address your inline feedback to make it much easier to review.

But if you feel, this isn't the right way to do it, i can split the PR.

- Restored original argparse boilerplate and  argument to minimize diff noise
- Added CLI arguments for , , and
- Removed hyphen-to-underscore conversion for camera topic names
- Removed redundant pa.Array type checking and length validation
- Removed debug prints, warnings, and verbose comments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants