OMPE-96097: Simplify kitless visual color randomization#6299
Conversation
…d OVRTX renderers randomize_visual_color only worked on the Kit/Replicator (IsaacRTX) path, so the kitless renderers had no way to randomize visual color at reset. Add per-prim AND per-env color randomization that honors an env_ids subset across all three backends, dispatched by sniffing the scene's renderer types. Each backend implements a uniform writer interface (constructed once from env + mesh_prim_path, then write_colors per reset): - NewtonShapeColorWriter: writes rows of model.shape_color directly (the live-array, no-notify path the Newton-Warp render context binds at load). Matches env-agnostically so a single source env_0 match recolors every cloned env, and skips /collisions/ shapes. - OVRTXVisualColorWriter: injects one OmniPBR material per target and rebinds each mesh via the renderer's write_attribute channel. Authoring is deferred to the first write_colors (post-bake, since the bake resets the runtime stage), and Renderer.reset() flushes the path-traced accumulator after every write. - Replicator path retained unchanged for Kit/IsaacRTX. Add a pre-PHYSICS_READY hook (EventManager.initialize_pre_physics_ready_terms) so writers can author USD before the OVRTX bake: OVRTX strips asset-bundled material:binding opinions there, otherwise the post-bake rebind is silently shadowed. Newton/Replicator are no-ops. The per-env target selection and shape validation shared by all backends live in isaaclab.utils.visual_color. Tests: one render-readback contract test per renderer (color lands, per-prim/per-env distinct, env_ids subset honored) driven from a shared scene helper and a bundled-OmniPBR cube fixture, plus writer unit tests and a pre-physics-ready-terms test.
Greptile SummaryThis PR replaces the original Kit-only Replicator event with a fully refactored, three-backend visual-color randomization system (RTX/Isaac, kitless Newton, OVRTX). It introduces a shared backend-neutral
Confidence Score: 4/5Safe to merge with minor follow-up; the new multi-backend architecture is solid and the test suite is thorough across all three renderer paths. The rework is well-structured and heavily tested (69 tests across RTX, Newton, OVRTX, and unit suites). Two style-level concerns are worth noting: the colors identity dispatch in randomize_visual_color.call creates an undocumented coupling to the EventManager argument-passing behaviour, and the _find_matching_visual_prims token regex can misparse alternation body-name patterns, adding unexpected false-positive prims to the proxy-walk candidates. Neither causes incorrect output on any known codepath. source/isaaclab/isaaclab/utils/visual_color.py (regex token BFS) and source/isaaclab/isaaclab/envs/mdp/events.py (call color dispatch) warrant a second look; all other files are clean. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant U as User / EnvCfg
participant EM as EventManager
participant RVC as randomize_visual_color
participant VCU as visual_color utils
participant W as Backend Writer
U->>EM: __init__(cfg)
EM->>EM: deepcopy(term_cfg) + validate mode
EM->>RVC: prepare_before_physics_ready(name, cfg, env)
RVC->>VCU: resolve_visual_color_target_plan()
VCU-->>RVC: VisualColorTargetPlan
RVC->>W: pre_physics_ready_setup(env, plan)
Note over W: RTX: de-instance prims
Note over W: OVRTX: author USD materials
Note over W: Newton: no-op
RVC->>RVC: store plan on cfg.__dict__
Note over EM,RVC: PHYSICS_READY
EM->>RVC: __init__(cfg, env)
RVC->>RVC: pop plan from cfg.__dict__
Note over EM,W: First reset
EM->>RVC: __call__(env, env_ids, colors)
RVC->>RVC: normalize env_ids to target_indices
RVC->>RVC: _ensure_writer() lazy construct
RVC->>W: write_colors(target_indices, random_colors)
W-->>RVC: colors written to renderer
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant U as User / EnvCfg
participant EM as EventManager
participant RVC as randomize_visual_color
participant VCU as visual_color utils
participant W as Backend Writer
U->>EM: __init__(cfg)
EM->>EM: deepcopy(term_cfg) + validate mode
EM->>RVC: prepare_before_physics_ready(name, cfg, env)
RVC->>VCU: resolve_visual_color_target_plan()
VCU-->>RVC: VisualColorTargetPlan
RVC->>W: pre_physics_ready_setup(env, plan)
Note over W: RTX: de-instance prims
Note over W: OVRTX: author USD materials
Note over W: Newton: no-op
RVC->>RVC: store plan on cfg.__dict__
Note over EM,RVC: PHYSICS_READY
EM->>RVC: __init__(cfg, env)
RVC->>RVC: pop plan from cfg.__dict__
Note over EM,W: First reset
EM->>RVC: __call__(env, env_ids, colors)
RVC->>RVC: normalize env_ids to target_indices
RVC->>RVC: _ensure_writer() lazy construct
RVC->>W: write_colors(target_indices, random_colors)
W-->>RVC: colors written to renderer
|
| if colors is self.cfg.params.get("colors"): | ||
| color_low, color_high = self._color_low, self._color_high | ||
| else: | ||
| colors = colors if colors else self._cfg.params.get("colors") | ||
|
|
||
| # parse the colors into replicator format | ||
| if isinstance(colors, dict): | ||
| # (r, g, b) - low, high --> (low_r, low_g, low_b) and (high_r, high_g, high_b) | ||
| color_low = [colors[key][0] for key in ["r", "g", "b"]] | ||
| color_high = [colors[key][1] for key in ["r", "g", "b"]] | ||
| colors = [color_low, color_high] | ||
| else: | ||
| colors = list(colors) | ||
|
|
||
| num_prims = len(self.material_prims) | ||
| random_colors = self.color_rng.generator.uniform(colors[0], colors[1], size=(num_prims, 3)) | ||
|
|
||
| rep.functional.modify.attribute(self.material_prims, "diffuse_color_constant", random_colors) | ||
| color_low_cpu, color_high_cpu = _visual_color_range(colors, "cpu") | ||
| color_low = color_low_cpu.to(env.device) | ||
| color_high = color_high_cpu.to(env.device) |
There was a problem hiding this comment.
Object-identity cache bypass is fragile.
colors is self.cfg.params.get("colors") works today because EventManager passes kwarg values directly from cfg.params without copying, so the colors argument and the stored value are the same Python object. If the dispatch layer ever wraps or reconstructs params, this check would always be False, silently moving every call site onto the per-call _visual_color_range validation path and allocating new device tensors every reset. A sentinel or an explicit _override_colors keyword would make the contract visible.
| if colors is self.cfg.params.get("colors"): | |
| color_low, color_high = self._color_low, self._color_high | |
| else: | |
| colors = colors if colors else self._cfg.params.get("colors") | |
| # parse the colors into replicator format | |
| if isinstance(colors, dict): | |
| # (r, g, b) - low, high --> (low_r, low_g, low_b) and (high_r, high_g, high_b) | |
| color_low = [colors[key][0] for key in ["r", "g", "b"]] | |
| color_high = [colors[key][1] for key in ["r", "g", "b"]] | |
| colors = [color_low, color_high] | |
| else: | |
| colors = list(colors) | |
| num_prims = len(self.material_prims) | |
| random_colors = self.color_rng.generator.uniform(colors[0], colors[1], size=(num_prims, 3)) | |
| rep.functional.modify.attribute(self.material_prims, "diffuse_color_constant", random_colors) | |
| color_low_cpu, color_high_cpu = _visual_color_range(colors, "cpu") | |
| color_low = color_low_cpu.to(env.device) | |
| color_high = color_high_cpu.to(env.device) | |
| # Use cached bounds when the caller passes the config's own colors object (the common | |
| # manager path). Any other object is treated as a per-call override and re-validated. | |
| if colors is self.cfg.params.get("colors"): | |
| color_low, color_high = self._color_low, self._color_high | |
| else: | |
| color_low_cpu, color_high_cpu = _visual_color_range(colors, "cpu") | |
| color_low = color_low_cpu.to(env.device) | |
| color_high = color_high_cpu.to(env.device) |
| for token in normalized_expr.split("/")[1:]: | ||
| token_pattern = re.compile(f"^{token}$") |
There was a problem hiding this comment.
Token-level regex compilation uses raw token strings as patterns without escaping.
re.compile(f"^{token}$") means a path component like env.0 would generate the pattern ^env.0$, which matches envX0, env_0, etc. USD prim names conventionally contain only [A-Za-z0-9_], so this is not an active bug, but adding re.escape for literal components makes the intent explicit and prevents silent mismatch if paths ever contain literal dots or other regex metacharacters.
| for token in normalized_expr.split("/")[1:]: | |
| token_pattern = re.compile(f"^{token}$") | |
| for token in normalized_expr.split("/")[1:]: | |
| # Escape literal components; leave explicit regex wildcards (.*) intact. | |
| safe_token = token if token == ".*" else re.escape(token) | |
| token_pattern = re.compile(f"^{safe_token}$") |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
dff38b4 to
86db906
Compare
86db906 to
b184f4e
Compare
Supersedes #6269 while preserving the two original Jessica Martinez commits and authorship.
Why
The original implementation proved the feature, but the first follow-up over-generalized it with shared clone-plan machinery, private renderer discovery, broad lifecycle changes, and excessive test duplication. This revision keeps the feature while removing those layers.
Scope
EventManagerhook that runs beforePHYSICS_READY. OVRTX must author materials before its camera callback snapshots the USD stage; normal reset/startup class terms are instantiated after that snapshot.ManagerBase,ManagerBasedEnv, orRenderContext.events.py.model.shape_colorthrough a zero-copy Torch view with linear-to-sRGB conversion.OVRTXRenderer.write_visual_colorsmethod.Size
Validation
16 passed: lifecycle, target selection, Newton writer, and OVRTX writer focused suites.6 passed: full real Isaac RTX color-randomization suite.1 passed: real RTX render-readback contract.isaaclab_ovpackage test tree; it skips locally because the optionalovrtxwheel is absent and will run in the package CI job.Deliberate constraints
replicate_physics=False.startupandreset, notprestartup.InteractiveSceneclone plans and requires a conventional/visualslayout or explicitmesh_name.