Commit 23064dc4 authored by matthewtrepte's avatar matthewtrepte Committed by Kelly Guo

Adds ovd animation recording feature (#429)

<!--
Thank you for your interest in sending a pull request. Please make sure
to check the contribution guidelines.

Link:
https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html
-->

Adds a feature where users can record a physics animation and bake the
operations into an ovd file.

<!-- As a practice, it is recommended to open an issue to have
discussions on the proposed pull request.
This makes it easier for the community to keep track of what is being
developed or added, and if a given feature
is demanded by more than one party. -->

<!-- As you go through the list, delete the ones that are not
applicable. -->

- Bug fix (non-breaking change which fixes an issue)
- New feature (non-breaking change which adds functionality)
- Breaking change (fix or feature that would cause existing
functionality to not work as expected)
- This change requires a documentation update

Please attach before and after screenshots of the change if applicable.

<!--
Example:

| Before | After |
| ------ | ----- |
| _gif/png before_ | _gif/png after_ |

To upload images to a PR -- simply drag and drop an image while in edit
mode and it should upload the image directly. You can then paste that
source into the above before/after sections.
-->

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [ ] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there

<!--
As you go through the checklist above, you can mark something as done by
putting an x character in it

For example,
- [x] I have done this task
- [ ] I have not done this task
-->

---------
Signed-off-by: 's avatarrwiltz <165190220+rwiltz@users.noreply.github.com>
Signed-off-by: 's avatarKelly Guo <kellyguo123@hotmail.com>
Signed-off-by: 's avatarAshwin Varghese Kuruttukulam <123109010+ashwinvkNV@users.noreply.github.com>
Signed-off-by: 's avatarKelly Guo <kellyg@nvidia.com>
Signed-off-by: 's avatarMichael Gussert <michael@gussert.com>
Co-authored-by: 's avatarjaczhangnv <jaczhang@nvidia.com>
Co-authored-by: 's avatarrwiltz <165190220+rwiltz@users.noreply.github.com>
Co-authored-by: 's avatarKelly Guo <kellyg@nvidia.com>
Co-authored-by: 's avatarYanzi Zhu <yanziz@nvidia.com>
Co-authored-by: 's avatarnv-mhaselton <mhaselton@nvidia.com>
Co-authored-by: 's avatarcosmith-nvidia <141183495+cosmith-nvidia@users.noreply.github.com>
Co-authored-by: 's avatarMichael Gussert <michael@gussert.com>
Co-authored-by: 's avatarCY Chen <cyc@nvidia.com>
Co-authored-by: 's avataroahmednv <oahmed@Nvidia.com>
Co-authored-by: 's avatarAshwin Varghese Kuruttukulam <123109010+ashwinvkNV@users.noreply.github.com>
Co-authored-by: 's avatarRafael Wiltz <rwiltz@nvidia.com>
Co-authored-by: 's avatarPeter Du <peterd@nvidia.com>
Co-authored-by: 's avatarKelly Guo <kellyguo123@hotmail.com>
Co-authored-by: 's avatarchengronglai <chengrongl@nvidia.com>
Co-authored-by: 's avatarpulkitg01 <pulkitg@nvidia.com>
Co-authored-by: 's avatarConnor Smith <cosmith@nvidia.com>
Co-authored-by: 's avatarAshwin Varghese Kuruttukulam <ashwinvk@nvidia.com>
Co-authored-by: 's avatarlotusl-code <lotusl@nvidia.com>
parent 25200735
...@@ -281,7 +281,7 @@ Changed ...@@ -281,7 +281,7 @@ Changed
:meth:`~isaaclab.utils.math.quat_apply` and :meth:`~isaaclab.utils.math.quat_apply_inverse` for speed. :meth:`~isaaclab.utils.math.quat_apply` and :meth:`~isaaclab.utils.math.quat_apply_inverse` for speed.
0.40.11 (2025-05-19) 0.40.12 (2025-05-19)
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
Fixed Fixed
...@@ -291,7 +291,7 @@ Fixed ...@@ -291,7 +291,7 @@ Fixed
of assets and sensors.used from the experience files and the double definition is removed. of assets and sensors.used from the experience files and the double definition is removed.
0.40.10 (2025-01-30) 0.40.11 (2025-01-30)
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
Added Added
...@@ -301,8 +301,8 @@ Added ...@@ -301,8 +301,8 @@ Added
in the simulation. in the simulation.
0.40.9 (2025-05-16) 0.40.10 (2025-05-16)
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
Added Added
^^^^^ ^^^^^
...@@ -316,7 +316,7 @@ Changed ...@@ -316,7 +316,7 @@ Changed
resampling call. resampling call.
0.40.8 (2025-05-16) 0.40.9 (2025-05-16)
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
Fixed Fixed
...@@ -325,7 +325,7 @@ Fixed ...@@ -325,7 +325,7 @@ Fixed
* Fixed penetration issue for negative border height in :class:`~isaaclab.terrains.terrain_generator.TerrainGeneratorCfg`. * Fixed penetration issue for negative border height in :class:`~isaaclab.terrains.terrain_generator.TerrainGeneratorCfg`.
0.40.7 (2025-05-20) 0.40.8 (2025-05-20)
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
Changed Changed
...@@ -340,7 +340,7 @@ Added ...@@ -340,7 +340,7 @@ Added
* Added :meth:`~isaaclab.utils.math.rigid_body_twist_transform` * Added :meth:`~isaaclab.utils.math.rigid_body_twist_transform`
0.40.6 (2025-05-15) 0.40.7 (2025-05-15)
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
Fixed Fixed
...@@ -354,14 +354,23 @@ Fixed ...@@ -354,14 +354,23 @@ Fixed
unused USD camera parameters. unused USD camera parameters.
0.40.5 (2025-05-14) 0.40.6 (2025-05-14)
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
* Added a new attribute :attr:`articulation_root_prim_path` to the :class:`~isaaclab.assets.ArticulationCfg` class * Added a new attribute :attr:`articulation_root_prim_path` to the :class:`~isaaclab.assets.ArticulationCfg` class
to allow explicitly specifying the prim path of the articulation root. to allow explicitly specifying the prim path of the articulation root.
0.40.4 (2025-05-14) 0.40.5 (2025-05-14)
~~~~~~~~~~~~~~~~~~~
Added
^^^^^
* Added feature for animation recording through baking physics operations into OVD files.
0.40.4 (2025-05-17)
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
Changed Changed
......
...@@ -134,6 +134,8 @@ class AppLauncher: ...@@ -134,6 +134,8 @@ class AppLauncher:
self._hide_stop_button() self._hide_stop_button()
# Set settings from the given rendering mode # Set settings from the given rendering mode
self._set_rendering_mode_settings(launcher_args) self._set_rendering_mode_settings(launcher_args)
# Set animation recording settings
self._set_animation_recording_settings(launcher_args)
# Hide play button callback if the timeline is stopped # Hide play button callback if the timeline is stopped
import omni.timeline import omni.timeline
...@@ -336,6 +338,29 @@ class AppLauncher: ...@@ -336,6 +338,29 @@ class AppLauncher:
' Example usage: --kit_args "--ext-folder=/path/to/ext1 --ext-folder=/path/to/ext2"' ' Example usage: --kit_args "--ext-folder=/path/to/ext1 --ext-folder=/path/to/ext2"'
), ),
) )
arg_group.add_argument(
"--anim_recording_enabled",
action="store_true",
help="Enable recording time-sampled USD animations from IsaacLab PhysX simulations.",
)
arg_group.add_argument(
"--anim_recording_start_time",
type=float,
default=0,
help=(
"Set time that animation recording begins playing. If not set, the recording will start from the"
" beginning."
),
)
arg_group.add_argument(
"--anim_recording_stop_time",
type=float,
default=10,
help=(
"Set time that animation recording stops playing. If the process is shutdown before the stop time is"
" exceeded, then the animation is not recorded."
),
)
# Corresponding to the beginning of the function, # Corresponding to the beginning of the function,
# if we have removed -h/--help handling, we add it back. # if we have removed -h/--help handling, we add it back.
...@@ -462,6 +487,9 @@ class AppLauncher: ...@@ -462,6 +487,9 @@ class AppLauncher:
# Handle experience file settings # Handle experience file settings
self._resolve_experience_file(launcher_args) self._resolve_experience_file(launcher_args)
# Handle animation recording settings
self._resolve_anim_recording_settings(launcher_args)
# Handle additional arguments # Handle additional arguments
self._resolve_kit_args(launcher_args) self._resolve_kit_args(launcher_args)
...@@ -718,6 +746,16 @@ class AppLauncher: ...@@ -718,6 +746,16 @@ class AppLauncher:
self._sim_experience_file = os.path.abspath(self._sim_experience_file) self._sim_experience_file = os.path.abspath(self._sim_experience_file)
print(f"[INFO][AppLauncher]: Loading experience file: {self._sim_experience_file}") print(f"[INFO][AppLauncher]: Loading experience file: {self._sim_experience_file}")
def _resolve_anim_recording_settings(self, launcher_args: dict):
"""Resolve animation recording settings."""
# Enable omni.physx.pvd extension if recording is enabled
recording_enabled = launcher_args.get("anim_recording_enabled", False)
if recording_enabled:
if self._headless:
raise ValueError("Animation recording is not supported in headless mode.")
sys.argv += ["--enable", "omni.physx.pvd"]
def _resolve_kit_args(self, launcher_args: dict): def _resolve_kit_args(self, launcher_args: dict):
"""Resolve additional arguments passed to Kit.""" """Resolve additional arguments passed to Kit."""
# Resolve additional arguments passed to Kit # Resolve additional arguments passed to Kit
...@@ -839,6 +877,33 @@ class AppLauncher: ...@@ -839,6 +877,33 @@ class AppLauncher:
key = "/" + key.replace(".", "/") # convert to carb setting format key = "/" + key.replace(".", "/") # convert to carb setting format
set_carb_setting(carb_setting, key, value) set_carb_setting(carb_setting, key, value)
def _set_animation_recording_settings(self, launcher_args: dict) -> None:
"""Set animation recording settings."""
import carb
from isaacsim.core.utils.carb import set_carb_setting
# check if recording is enabled
recording_enabled = launcher_args.get("anim_recording_enabled", False)
if not recording_enabled:
return
# arg checks
if launcher_args.get("anim_recording_start_time") >= launcher_args.get("anim_recording_stop_time"):
raise ValueError(
f"'anim_recording_start_time' {launcher_args.get('anim_recording_start_time')} must be less than"
f" 'anim_recording_stop_time' {launcher_args.get('anim_recording_stop_time')}"
)
# grab config
start_time = launcher_args.get("anim_recording_start_time")
stop_time = launcher_args.get("anim_recording_stop_time")
# store config in carb settings
carb_settings = carb.settings.get_settings()
set_carb_setting(carb_settings, "/isaaclab/anim_recording/enabled", recording_enabled)
set_carb_setting(carb_settings, "/isaaclab/anim_recording/start_time", start_time)
set_carb_setting(carb_settings, "/isaaclab/anim_recording/stop_time", stop_time)
def _interrupt_signal_handle_callback(self, signal, frame): def _interrupt_signal_handle_callback(self, signal, frame):
"""Handle the interrupt signal from the keyboard.""" """Handle the interrupt signal from the keyboard."""
# close the app # close the app
......
...@@ -5,14 +5,18 @@ ...@@ -5,14 +5,18 @@
import builtins import builtins
import enum import enum
import glob
import numpy as np import numpy as np
import os import os
import re
import time
import toml import toml
import torch import torch
import traceback import traceback
import weakref import weakref
from collections.abc import Iterator from collections.abc import Iterator
from contextlib import contextmanager from contextlib import contextmanager
from datetime import datetime
from typing import Any from typing import Any
import carb import carb
...@@ -138,6 +142,9 @@ class SimulationContext(_SimulationContext): ...@@ -138,6 +142,9 @@ class SimulationContext(_SimulationContext):
# read flag for whether XR GUI is enabled # read flag for whether XR GUI is enabled
self._xr_gui = self.carb_settings.get("/app/xr/enabled") self._xr_gui = self.carb_settings.get("/app/xr/enabled")
# read flags anim recording config and init timestamps
self._setup_anim_recording()
# read flag for whether the Isaac Lab viewport capture pipeline will be used, # read flag for whether the Isaac Lab viewport capture pipeline will be used,
# casting None to False if the flag doesn't exist # casting None to False if the flag doesn't exist
# this flag is set from the AppLauncher class # this flag is set from the AppLauncher class
...@@ -559,6 +566,14 @@ class SimulationContext(_SimulationContext): ...@@ -559,6 +566,14 @@ class SimulationContext(_SimulationContext):
exception_to_raise = builtins.ISAACLAB_CALLBACK_EXCEPTION exception_to_raise = builtins.ISAACLAB_CALLBACK_EXCEPTION
builtins.ISAACLAB_CALLBACK_EXCEPTION = None builtins.ISAACLAB_CALLBACK_EXCEPTION = None
raise exception_to_raise raise exception_to_raise
# update anim recording if needed
if self._anim_recording_enabled:
is_anim_recording_finished = self._update_anim_recording()
if is_anim_recording_finished:
carb.log_warn("[INFO][SimulationContext]: Animation recording finished. Closing app.")
self._app.shutdown()
# check if the simulation timeline is paused. in that case keep stepping until it is playing # check if the simulation timeline is paused. in that case keep stepping until it is playing
if not self.is_playing(): if not self.is_playing():
# step the simulator (but not the physics) to have UI still active # step the simulator (but not the physics) to have UI still active
...@@ -744,6 +759,119 @@ class SimulationContext(_SimulationContext): ...@@ -744,6 +759,119 @@ class SimulationContext(_SimulationContext):
# Needed for backward compatibility with older Isaac Sim versions # Needed for backward compatibility with older Isaac Sim versions
self._update_fabric = self._fabric_iface.update self._update_fabric = self._fabric_iface.update
def _update_anim_recording(self):
"""Tracks anim recording timestamps and triggers finish animation recording if the total time has elapsed."""
if self._anim_recording_started_timestamp is None:
self._anim_recording_started_timestamp = time.time()
if self._anim_recording_started_timestamp is not None:
anim_recording_total_time = time.time() - self._anim_recording_started_timestamp
if anim_recording_total_time > self._anim_recording_stop_time:
self._finish_anim_recording()
return True
return False
def _setup_anim_recording(self):
"""Sets up anim recording settings and initializes the recording."""
self._anim_recording_enabled = bool(self.carb_settings.get("/isaaclab/anim_recording/enabled"))
if not self._anim_recording_enabled:
return
# Import omni.physx.pvd.bindings here since it is not available by default
from omni.physxpvd.bindings import _physxPvd
# Init anim recording settings
self._anim_recording_start_time = self.carb_settings.get("/isaaclab/anim_recording/start_time")
self._anim_recording_stop_time = self.carb_settings.get("/isaaclab/anim_recording/stop_time")
self._anim_recording_first_step_timestamp = None
self._anim_recording_started_timestamp = None
# Make output path relative to repo path
repo_path = os.path.join(carb.tokens.get_tokens_interface().resolve("${app}"), "..")
self._anim_recording_timestamp = datetime.now().strftime("%Y_%m_%d_%H%M%S")
self._anim_recording_output_dir = (
os.path.join(repo_path, "anim_recordings", self._anim_recording_timestamp).replace("\\", "/").rstrip("/")
+ "/"
)
os.makedirs(self._anim_recording_output_dir, exist_ok=True)
# Acquire physx pvd interface and set output directory
self._physxPvdInterface = _physxPvd.acquire_physx_pvd_interface()
# Set carb settings for the output path and enabling pvd recording
set_carb_setting(
self.carb_settings, "/persistent/physics/omniPvdOvdRecordingDirectory", self._anim_recording_output_dir
)
set_carb_setting(self.carb_settings, "/physics/omniPvdOutputEnabled", True)
def _update_usda_start_time(self, file_path, start_time):
"""Updates the start time of the USDA baked anim recordingfile."""
# Read the USDA file
with open(file_path) as file:
content = file.read()
# Extract the timeCodesPerSecond value
time_code_match = re.search(r"timeCodesPerSecond\s*=\s*(\d+)", content)
if not time_code_match:
raise ValueError("timeCodesPerSecond not found in the file.")
time_codes_per_second = int(time_code_match.group(1))
# Compute the new start time code
new_start_time_code = int(start_time * time_codes_per_second)
# Replace the startTimeCode in the file
content = re.sub(r"startTimeCode\s*=\s*\d+", f"startTimeCode = {new_start_time_code}", content)
# Write the updated content back to the file
with open(file_path, "w") as file:
file.write(content)
def _finish_anim_recording(self):
"""Finishes the animation recording and outputs the baked animation recording."""
carb.log_warn(
"[INFO][SimulationContext]: Finishing animation recording. Stage must be saved. Might take a few minutes."
)
# Detaching the stage will also close it and force the serialization of the OVD file
physx = omni.physx.get_physx_simulation_interface()
physx.detach_stage()
# Save stage to disk
stage_path = os.path.join(self._anim_recording_output_dir, "stage_simulation.usdc")
stage_utils.save_stage(stage_path, save_and_reload_in_place=False)
# Find the latest ovd file not named tmp.ovd
ovd_files = [
f for f in glob.glob(os.path.join(self._anim_recording_output_dir, "*.ovd")) if not f.endswith("tmp.ovd")
]
input_ovd_path = max(ovd_files, key=os.path.getctime)
# Invoke pvd interface to create recording
stage_filename = "baked_animation_recording.usda"
result = self._physxPvdInterface.ovd_to_usd_over_with_layer_creation(
input_ovd_path,
stage_path,
self._anim_recording_output_dir,
stage_filename,
self._anim_recording_start_time,
self._anim_recording_stop_time,
True, # True: ASCII layers / False : USDC layers
False, # True: verify over layer
)
# Workaround for manually setting the truncated start time in the baked animation recording
self._update_usda_start_time(
os.path.join(self._anim_recording_output_dir, stage_filename), self._anim_recording_start_time
)
# Disable recording
set_carb_setting(self.carb_settings, "/physics/omniPvdOutputEnabled", False)
return result
""" """
Callbacks. Callbacks.
""" """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment