├── .gitignore ├── lightwheel └── MJCF2USD │ └── connection │ ├── __init__.py │ ├── extension.py │ ├── window.py │ ├── ui_utils.py │ ├── option_widget.py │ ├── style.py │ └── mjcf2usd_utils.py ├── data ├── logo.png └── preview.jpeg ├── Installation.pdf ├── config └── extension.toml ├── README.md └── LICENSE.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /lightwheel/MJCF2USD/connection/__init__.py: -------------------------------------------------------------------------------- 1 | from .extension import * 2 | -------------------------------------------------------------------------------- /data/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LightwheelAI/mjcf2usd/HEAD/data/logo.png -------------------------------------------------------------------------------- /Installation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LightwheelAI/mjcf2usd/HEAD/Installation.pdf -------------------------------------------------------------------------------- /data/preview.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LightwheelAI/mjcf2usd/HEAD/data/preview.jpeg -------------------------------------------------------------------------------- /lightwheel/MJCF2USD/connection/extension.py: -------------------------------------------------------------------------------- 1 | import omni.ext 2 | import omni.ui as ui 3 | from .window import MJCF2USDWindow 4 | 5 | # Any class derived from `omni.ext.IExt` in top level module (defined in `python.modules` of `extension.toml`) will be 6 | # instantiated when extension gets enabled and `on_startup(ext_id)` will be called. Later when extension gets disabled 7 | # on_shutdown() is called. 8 | class MJCF2USDExt(omni.ext.IExt): 9 | # ext_id is current extension id. It can be used with extension manager to query additional information, like where 10 | # this extension is located on filesystem. 11 | def on_startup(self, ext_id): 12 | print("[MJCF2USD.Ext] startup") 13 | 14 | self._window = MJCF2USDWindow("MJCF2USD", width=500, height=500) 15 | 16 | 17 | def on_shutdown(self): 18 | self._window.destroy() 19 | print("[MJCF2USD.Ext] shutdown") 20 | -------------------------------------------------------------------------------- /config/extension.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | # Semantic Versioning is used: https://semver.org/ 3 | version = "1.0.0" 4 | 5 | # Lists people or organizations that are considered the "authors" of the package. 6 | authors = ["LIGHTWHEEL"] 7 | 8 | # The title and description fields are primarily for displaying extension info in UI 9 | title = "MJCF2USD" 10 | description="Transform MJCF to USD" 11 | 12 | # Path (relative to the root) or content of readme markdown file for UI. 13 | readme = "docs/README.md" 14 | 15 | # URL of the extension source repository. 16 | repository = "" 17 | 18 | # One of categories for UI. 19 | category = "Simulation" 20 | 21 | # Keywords for the extension 22 | keywords = ["kit", "simulation"] 23 | 24 | # Location of change log file in target (final) folder of extension, relative to the root. 25 | # More info on writing changelog: https://keepachangelog.com/en/1.0.0/ 26 | changelog="docs/CHANGELOG.md" 27 | 28 | # Preview image and icon. Folder named "data" automatically goes in git lfs (see .gitattributes file). 29 | # Preview image is shown in "Overview" of Extensions window. Screenshot of an extension might be a good preview image. 30 | preview_image = "data/preview.jpeg" 31 | 32 | # Icon is shown in Extensions window, it is recommended to be square, of size 256x256. 33 | icon = "data/logo.png" 34 | 35 | # Use omni.ui to build simple UI 36 | [dependencies] 37 | "omni.kit.uiapp" = {} 38 | 39 | # Main python module this extension provides, it will be publicly available as "import company.hello.world". 40 | [[python.module]] 41 | name = "lightwheel.MJCF2USD.connection" 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MJCF2USD 2 | 3 | A powerful Isaac Sim extension for converting MuJoCo XML Configuration Format (MJCF) files to Universal Scene Description (USD) format. 4 | 5 | ***Welcome to the Lightwheel open-source community!*** 6 | 7 | Join us, contribute, and help shape the future of AI and robotics. 8 | For questions or collaboration, contact Frank Chen at ming.chen@lightwheel.ai. 9 | 10 | 11 | 12 | ## Overview 13 | 14 | MJCF2USD is a user-friendly tool that enables seamless conversion of MuJoCo simulation files to USD format, making it easy to integrate MuJoCo models into Omniverse workflows and other USD-compatible applications. 15 | 16 | ## Features 17 | 18 | - **Batch Conversion**: Convert multiple MJCF files at once 19 | - **Intuitive UI**: Simple and clean interface with step-by-step guidance 20 | - **Flexible Output**: Choose custom output locations or use default paths 21 | - **Progress Tracking**: Real-time conversion progress with time estimates 22 | - **Error Handling**: Detailed success/failure reporting 23 | - **XML Modification**: Optional temporary XML file saving for debugging 24 | 25 | ## Installation 26 | 27 | ### Prerequisites 28 | 29 | - NVIDIA Isaac Sim 30 | 31 | ### Installation Steps 32 | 33 | 1. Clone this repository to your Omniverse extensions directory: 34 | ```bash 35 | git clone /path/to/isaacsim/extensions 36 | ``` 37 | 38 | 2. Enable the extension through the Extensions Manager in Isaac Sim. 39 | 40 | 3. **Set the Import Path in Isaac Sim:** 41 | - Open Isaac Sim and go to `Window` > `Extensions`. 42 | - Click the `Settings` (gear) icon. 43 | - In the `Extension Search Paths` section, click the `+` button. 44 | - Copy and paste the parent path of the `mjcf2usd` folder (the path where you cloned this repository). 45 | 46 | > 📄 **For detailed step-by-step instructions with screenshots, please refer to [Installation.pdf](Installation.pdf) included in this repository.** 47 | 48 | ## Usage 49 | 50 | ### Getting Started 51 | 52 | 1. **Launch the Extension**: 53 | - Open IsaacSim 54 | - Go to `Window` > `Extensions`. 55 | - Enable "MJCF2USD" extension 56 | - The MJCF2USD window will appear 57 | 58 | 2. **Select MJCF Files**: 59 | - Click "Select File or Folder" in step 1 60 | - Choose either a single MJCF file or a folder containing multiple MJCF files 61 | - The extension will automatically scan and list all found MJCF files 62 | 63 | 3. **Choose Output Location** (Optional): 64 | - Click "Select Folder" in step 2 65 | - Choose where you want the USD files to be saved 66 | - If left empty, USD files will be saved in the same directory as the MJCF files 67 | 68 | 4. **Configure Options**: 69 | - Check "Save Temp MJCF XML" if you want to save modified XML files for debugging 70 | 71 | 5. **Start Conversion**: 72 | - Click "MJCFs to USDs" button 73 | - Monitor the progress in the status area 74 | - View conversion results and timing information 75 | 76 | ### Output 77 | 78 | - **Success**: USD files will be created in the specified output location 79 | - **File Naming**: Output files follow the pattern `{parent_folder}_{filename}.usd` 80 | - **Reports**: Detailed conversion statistics including: 81 | - Total conversion time 82 | - Number of successful conversions 83 | - Number of failed conversions 84 | - List of successful and failed files 85 | 86 | ## Project Structure 87 | 88 | ``` 89 | MJCF2USD/ 90 | ├── config/ 91 | │ └── extension.toml # Extension configuration 92 | ├── data/ 93 | │ ├── logo.png # Extension icon 94 | │ └── preview.jpeg # Extension preview image 95 | ├── lightwheel/ 96 | │ └── MJCF2USD/ 97 | │ └── connection/ 98 | │ ├── extension.py # Main extension entry point 99 | │ ├── window.py # UI window implementation 100 | │ ├── mjcf2usd_utils.py # Core conversion utilities 101 | │ ├── option_widget.py # UI components 102 | │ ├── ui_utils.py # UI utility functions 103 | │ └── style.py # UI styling 104 | ├── LICENSE.txt # License file 105 | └── README.md # This file 106 | ``` 107 | 108 | ## Development 109 | 110 | ### Dependencies 111 | 112 | - `omni.kit.uiapp`: Omniverse UI framework 113 | - `omni.ui`: Omniverse UI components 114 | - `omni.usd`: USD manipulation utilities 115 | - `omni.kit.commands`: Omniverse command system 116 | 117 | ### Building 118 | 119 | The extension is built using Omniverse Kit's extension system. No additional build steps are required beyond the standard Python module structure. 120 | 121 | ## Version 122 | 123 | Current version: 1.0.0 124 | 125 | ## Support 126 | 127 | For issues and questions, please contact the development team or create an issue in the repository. 128 | -------------------------------------------------------------------------------- /lightwheel/MJCF2USD/connection/window.py: -------------------------------------------------------------------------------- 1 | import os 2 | import xml.etree.ElementTree as ET 3 | import omni.kit.commands 4 | import omni.ui as ui 5 | import omni.usd 6 | from .mjcf2usd_utils import get_xmls,mjcf_to_usd 7 | from .option_widget import string_filed_builder 8 | import time 9 | 10 | class MJCF2USDWindow(ui.Window): 11 | 12 | def __init__(self, title: str, **kwargs) -> None: 13 | super().__init__(title, **kwargs) 14 | self._models = { 15 | "usd_success":[], 16 | "usd_failed":[], 17 | "convert_time":[], 18 | } 19 | self._xmls = [] 20 | self._stage = omni.usd.get_context().get_stage() 21 | self.frame.set_build_fn(self._build_fn) 22 | self.label_style_red = { 23 | "font_size":18, 24 | "font_weight":"bold", 25 | "color":0xFF0000FF, 26 | } 27 | self.label_style_green = { 28 | "font_size":18, 29 | "font_weight":"bold", 30 | "color":0xFF66CD00, 31 | } 32 | 33 | 34 | def _build_fn(self): 35 | with ui.ScrollingFrame(): 36 | with ui.VStack(height=10): 37 | self._build_window() 38 | 39 | def destroy(self) -> None: 40 | return super().destroy() 41 | 42 | 43 | def _build_window(self): 44 | self._stage = omni.usd.get_context().get_stage() 45 | with self.frame: 46 | with ui.VStack(spacing=4,height=0): 47 | 48 | 49 | self._mjcf_location_label = ui.Label("[1] : Select MJCF Location", 50 | style=self.label_style_red) 51 | 52 | self._models["mjcf_root_path"] = string_filed_builder( 53 | tooltip="Select MJCF or folder containing MJCFs to convert", 54 | default_val="No MJCF or folder containing MJCFs be selected", 55 | folder_dialog_title="Select MJCF File or Folder containing MJCFs", 56 | folder_button_title="Select File or Folder", 57 | read_only=False, 58 | ) 59 | 60 | self._usd_location_label = ui.Label("[2] : Select USD Location", 61 | style=self.label_style_red) 62 | 63 | self._models["usd_root_path"] = string_filed_builder( 64 | tooltip="Select the storage location of USDs (default is the same as the MJCF location)", 65 | default_val="None, default is the same as the MJCF location", 66 | folder_dialog_title="Select the storage location of USDs", 67 | folder_button_title="Select Folder", 68 | read_only=False, 69 | ) 70 | 71 | 72 | 73 | with ui.HStack(height=0): 74 | self._modify_xml_checkbox = ui.CheckBox( 75 | width=20, 76 | height=20, 77 | style={ 78 | "margin": 2, 79 | "color": 0xFF000000, 80 | "background_color": 0xFFFFFFFF, 81 | "border_radius": 2, 82 | "border_width": 1, 83 | "border_color": 0xFF808080, 84 | } 85 | ) 86 | ui.Spacer(width=10) 87 | ui.Label( 88 | "Save Temp MJCF XML", 89 | style=self.label_style_green 90 | ) 91 | 92 | self._mjcf2usd_button = ui.Button( 93 | "MJCFs to USDs", 94 | clicked_fn=self._on_xmls2usd, 95 | enabled=False, 96 | style={ 97 | "font_size": 14, 98 | "font_weight": "bold", 99 | "color": 0xFF000000, 100 | "background_color": 0xFFE0E0E0, 101 | "border_radius": 4, # 圆角 102 | "border_width": 1, 103 | "border_color": 0xFF808080, 104 | "margin": 2, 105 | "padding": 4, 106 | ":hover": { 107 | "background_color": 0xFFD0D0D0, 108 | "border_color": 0xFF606060, 109 | }, 110 | ":pressed": { 111 | "background_color": 0xFFC0C0C0, 112 | "translate": [1, 1], 113 | }, 114 | "transition": { 115 | "background_color": 0.2, 116 | "scale": 0.02, 117 | "translate": 0.02, 118 | } 119 | } 120 | ) 121 | 122 | self._mjcf2usd_label = ui.Label("[3] : Begin to convert MJCFs to USDs", 123 | style=self.label_style_green) 124 | 125 | self._mjcf2usd_label.visible = False 126 | 127 | self._message_label = ui.Label( 128 | "No MJCFs found", 129 | style=self.label_style_red, 130 | ) 131 | 132 | def _on_mjcf_location_changed(model): 133 | value = model.get_value_as_string() 134 | if "\\" in value or "/" in value: 135 | self._xmls = get_xmls(value) 136 | if self._xmls: 137 | self._mjcf_location_label.style = self.label_style_green 138 | self._usd_location_label.style = self.label_style_green 139 | self._mjcf2usd_label.style = self.label_style_green 140 | self._mjcf2usd_button.enabled = True 141 | xml_text = "\n".join(self._xmls) 142 | self._message_label.text = f"Found {len(self._xmls)} MJCFs:\n{xml_text}" 143 | self._message_label.style = self.label_style_green 144 | else: 145 | self._message_label.text = "No MJCFs found" 146 | self._message_label.style = self.label_style_red 147 | self._mjcf_location_label.style = self.label_style_red 148 | self._usd_location_label.style = self.label_style_red 149 | self._mjcf2usd_label.style = self.label_style_red 150 | self._mjcf2usd_button.enabled = False 151 | # self._mjcf2usd_progress_bar.visible = False 152 | self._mjcf2usd_label.visible = False 153 | else: 154 | self._mjcf_location_label.style = self.label_style_red 155 | self._usd_location_label.style = self.label_style_red 156 | self._mjcf2usd_label.style = self.label_style_red 157 | self._mjcf2usd_button.enabled = False 158 | # self._mjcf2usd_progress_bar.visible = False 159 | self._mjcf2usd_label.visible = False 160 | 161 | self._models["mjcf_root_path"].add_value_changed_fn(_on_mjcf_location_changed) 162 | 163 | 164 | def xmls2usd(self): 165 | self._models["convert_time"] = [] 166 | for i ,xml in enumerate(self._xmls): 167 | time_start = time.time() 168 | if 'None' in self._models["usd_root_path"].get_value_as_string(): 169 | parent_dir = os.path.dirname(xml) 170 | model_name = os.path.basename(parent_dir) 171 | usd_name = str(model_name) + '.usd' 172 | usd_path = os.path.join(parent_dir, usd_name) 173 | else: 174 | parent_dir = os.path.dirname(xml) 175 | model_name = os.path.basename(parent_dir) 176 | usd_name = str(model_name) + '.usd' 177 | 178 | filepath = os.path.join(self._models["usd_root_path"].get_value_as_string(),model_name) 179 | if not os.path.exists(filepath): 180 | os.mkdir(filepath) 181 | 182 | usd_path = os.path.join(filepath,usd_name) 183 | 184 | need_modify_xml = self._modify_xml_checkbox.model.get_value_as_bool() 185 | mjcf_to_usd(xml, usd_path,need_modify_xml) 186 | if os.path.exists(usd_path): 187 | self._models["usd_success"].append(usd_path) 188 | else: 189 | self._models["usd_failed"].append(xml) 190 | time_end = time.time() 191 | self._models["convert_time"].append(time_end-time_start) 192 | time_total = sum(self._models["convert_time"]) 193 | time_average = time_total/len(self._xmls) 194 | time_left = time_average*(len(self._xmls)-i-1) 195 | # self._message_label.text = f"Converting MJCFs to USDs: {i+1}/{len(self._xmls)}" + \ 196 | # f" Time left: {time_left:.2f} seconds" 197 | success_text = "\n".join(self._models["usd_success"]) 198 | failed_text = "\n".join(self._models["usd_failed"]) 199 | self._message_label.text += f"\n[Total time]: {time_total:.2f} seconds" + \ 200 | f"\n[Success number]: {len(self._models['usd_success'])}" + \ 201 | f"\n[Failed number]: {len(self._models['usd_failed'])}" + \ 202 | f"\n\n[Success files]: \n{success_text}" + \ 203 | f"\n\n[Failed files]: \n{failed_text}" 204 | 205 | def _on_xmls2usd(self): 206 | self._mjcf2usd_label.visible = True 207 | # self._mjcf2usd_progress_bar.visible = True 208 | self._mjcf2usd_button.enabled = False 209 | # self._mjcf2usd_progress_bar.model.set_value(0) 210 | self.xmls2usd() 211 | self._models["convert_time"] = [] 212 | self._models["usd_success"] = [] 213 | self._models["usd_failed"] = [] 214 | self._mjcf2usd_button.enabled = True 215 | 216 | 217 | -------------------------------------------------------------------------------- /lightwheel/MJCF2USD/connection/ui_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018-2023, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # NVIDIA CORPORATION and its licensors retain all intellectual property 4 | # and proprietary rights in and to this software, related documentation 5 | # and any modifications thereto. Any use, reproduction, disclosure or 6 | # distribution of this software and related documentation without an express 7 | # license agreement from NVIDIA CORPORATION is strictly prohibited. 8 | # 9 | 10 | # str_builder 11 | 12 | import asyncio 13 | import os 14 | import subprocess 15 | import sys 16 | from cmath import inf 17 | 18 | import carb.settings 19 | import omni.appwindow 20 | import omni.ext 21 | import omni.ui as ui 22 | from omni.kit.window.extensions import SimpleCheckBox 23 | from omni.kit.window.filepicker import FilePickerDialog 24 | from omni.kit.window.property.templates import LABEL_HEIGHT, LABEL_WIDTH 25 | 26 | # from .callbacks import on_copy_to_clipboard, on_docs_link_clicked, on_open_folder_clicked, on_open_IDE_clicked 27 | from .style import ( 28 | BUTTON_WIDTH, 29 | COLOR_W, 30 | COLOR_X, 31 | COLOR_Y, 32 | COLOR_Z, 33 | get_option_style, 34 | get_style, 35 | ) 36 | 37 | 38 | def add_line_rect_flourish(draw_line=True): 39 | """Aesthetic element that adds a Line + Rectangle after all UI elements in the row. 40 | 41 | Args: 42 | draw_line (bool, optional): Set false to only draw rectangle. Defaults to True. 43 | """ 44 | if draw_line: 45 | ui.Line(style={"color": 0x338A8777}, width=ui.Fraction(1), alignment=ui.Alignment.CENTER) 46 | ui.Spacer(width=10) 47 | with ui.Frame(width=0): 48 | with ui.VStack(): 49 | with ui.Placer(offset_x=0, offset_y=7): 50 | ui.Rectangle(height=5, width=5, alignment=ui.Alignment.CENTER) 51 | ui.Spacer(width=5) 52 | 53 | 54 | def format_tt(tt): 55 | import string 56 | 57 | formated = "" 58 | i = 0 59 | for w in tt.split(): 60 | if w.isupper(): 61 | formated += w + " " 62 | elif len(w) > 3 or i == 0: 63 | formated += string.capwords(w) + " " 64 | else: 65 | formated += w.lower() + " " 66 | i += 1 67 | return formated 68 | 69 | 70 | def add_folder_picker_icon( 71 | on_click_fn, 72 | item_filter_fn=None, 73 | bookmark_label=None, 74 | bookmark_path=None, 75 | dialog_title="Select Output Folder", 76 | button_title="Select Folder", 77 | size=24, 78 | ): 79 | def open_file_picker(): 80 | def on_selected(filename, path): 81 | on_click_fn(filename, path) 82 | file_picker.hide() 83 | 84 | def on_canceled(a, b): 85 | file_picker.hide() 86 | 87 | file_picker = FilePickerDialog( 88 | dialog_title, 89 | allow_multi_selection=False, 90 | apply_button_label=button_title, 91 | click_apply_handler=lambda a, b: on_selected(a, b), 92 | click_cancel_handler=lambda a, b: on_canceled(a, b), 93 | item_filter_fn=item_filter_fn, 94 | enable_versioning_pane=True, 95 | ) 96 | if bookmark_label and bookmark_path: 97 | file_picker.toggle_bookmark_from_path(bookmark_label, bookmark_path, True) 98 | 99 | with ui.Frame(width=0, tooltip=button_title): 100 | ui.Button( 101 | name="IconButton", 102 | width=size, 103 | height=size, 104 | clicked_fn=open_file_picker, 105 | style=get_style()["IconButton.Image::FolderPicker"], 106 | alignment=ui.Alignment.RIGHT_CENTER, 107 | ) 108 | return open_file_picker 109 | 110 | 111 | def btn_builder(label="", type="button", text="button", tooltip="", on_clicked_fn=None): 112 | """Creates a stylized button. 113 | 114 | Args: 115 | label (str, optional): Label to the left of the UI element. Defaults to "". 116 | type (str, optional): Type of UI element. Defaults to "button". 117 | text (str, optional): Text rendered on the button. Defaults to "button". 118 | tooltip (str, optional): Tooltip to display over the Label. Defaults to "". 119 | on_clicked_fn (Callable, optional): Call-back function when clicked. Defaults to None. 120 | 121 | Returns: 122 | ui.Button: Button 123 | """ 124 | with ui.HStack(): 125 | ui.Label(label, width=LABEL_WIDTH, alignment=ui.Alignment.LEFT_CENTER, tooltip=format_tt(tooltip)) 126 | btn = ui.Button( 127 | text.upper(), 128 | name="Button", 129 | width=BUTTON_WIDTH, 130 | clicked_fn=on_clicked_fn, 131 | style=get_style(), 132 | alignment=ui.Alignment.LEFT_CENTER, 133 | ) 134 | ui.Spacer(width=5) 135 | add_line_rect_flourish(True) 136 | # ui.Spacer(width=ui.Fraction(1)) 137 | # ui.Spacer(width=10) 138 | # with ui.Frame(width=0): 139 | # with ui.VStack(): 140 | # with ui.Placer(offset_x=0, offset_y=7): 141 | # ui.Rectangle(height=5, width=5, alignment=ui.Alignment.CENTER) 142 | # ui.Spacer(width=5) 143 | return btn 144 | 145 | 146 | def cb_builder(label="", type="checkbox", default_val=False, tooltip="", on_clicked_fn=None): 147 | """Creates a Stylized Checkbox 148 | 149 | Args: 150 | label (str, optional): Label to the left of the UI element. Defaults to "". 151 | type (str, optional): Type of UI element. Defaults to "checkbox". 152 | default_val (bool, optional): Checked is True, Unchecked is False. Defaults to False. 153 | tooltip (str, optional): Tooltip to display over the Label. Defaults to "". 154 | on_clicked_fn (Callable, optional): Call-back function when clicked. Defaults to None. 155 | 156 | Returns: 157 | ui.SimpleBoolModel: model 158 | """ 159 | 160 | with ui.HStack(): 161 | ui.Label(label, width=LABEL_WIDTH - 12, alignment=ui.Alignment.LEFT_CENTER, tooltip=format_tt(tooltip)) 162 | model = ui.SimpleBoolModel() 163 | callable = on_clicked_fn 164 | if callable is None: 165 | callable = lambda x: None 166 | SimpleCheckBox(default_val, callable, model=model) 167 | 168 | add_line_rect_flourish() 169 | return model 170 | 171 | 172 | def dropdown_builder( 173 | label="", type="dropdown", default_val=0, items=["Option 1", "Option 2", "Option 3"], tooltip="", on_clicked_fn=None 174 | ): 175 | """Creates a Stylized Dropdown Combobox 176 | 177 | Args: 178 | label (str, optional): Label to the left of the UI element. Defaults to "". 179 | type (str, optional): Type of UI element. Defaults to "dropdown". 180 | default_val (int, optional): Default index of dropdown items. Defaults to 0. 181 | items (list, optional): List of items for dropdown box. Defaults to ["Option 1", "Option 2", "Option 3"]. 182 | tooltip (str, optional): Tooltip to display over the Label. Defaults to "". 183 | on_clicked_fn (Callable, optional): Call-back function when clicked. Defaults to None. 184 | 185 | Returns: 186 | AbstractItemModel: model 187 | """ 188 | with ui.HStack(): 189 | ui.Label(label, width=LABEL_WIDTH, alignment=ui.Alignment.LEFT_CENTER, tooltip=format_tt(tooltip)) 190 | combo_box = ui.ComboBox( 191 | default_val, *items, name="ComboBox", width=ui.Fraction(1), alignment=ui.Alignment.LEFT_CENTER 192 | ).model 193 | add_line_rect_flourish(False) 194 | 195 | def on_clicked_wrapper(model, val): 196 | on_clicked_fn(items[model.get_item_value_model().as_int]) 197 | 198 | if on_clicked_fn is not None: 199 | combo_box.add_item_changed_fn(on_clicked_wrapper) 200 | 201 | return combo_box 202 | 203 | 204 | def float_builder(label="", type="floatfield", default_val=0, tooltip="", min=-inf, max=inf, step=0.1, format="%.2f"): 205 | """Creates a Stylized Floatfield Widget 206 | 207 | Args: 208 | label (str, optional): Label to the left of the UI element. Defaults to "". 209 | type (str, optional): Type of UI element. Defaults to "floatfield". 210 | default_val (int, optional): Default Value of UI element. Defaults to 0. 211 | tooltip (str, optional): Tooltip to display over the UI elements. Defaults to "". 212 | 213 | Returns: 214 | AbstractValueModel: model 215 | """ 216 | with ui.HStack(): 217 | ui.Label(label, width=LABEL_WIDTH, alignment=ui.Alignment.LEFT_CENTER, tooltip=format_tt(tooltip)) 218 | float_field = ui.FloatDrag( 219 | name="FloatField", 220 | width=ui.Fraction(1), 221 | height=0, 222 | alignment=ui.Alignment.LEFT_CENTER, 223 | min=min, 224 | max=max, 225 | step=step, 226 | format=format, 227 | ).model 228 | float_field.set_value(default_val) 229 | add_line_rect_flourish(False) 230 | return float_field 231 | 232 | 233 | def str_builder( 234 | label="", 235 | type="stringfield", 236 | default_val=" ", 237 | tooltip="", 238 | on_clicked_fn=None, 239 | use_folder_picker=False, 240 | read_only=False, 241 | item_filter_fn=None, 242 | bookmark_label=None, 243 | bookmark_path=None, 244 | folder_dialog_title="Select Output Folder", 245 | folder_button_title="Select Folder", 246 | ): 247 | """Creates a Stylized Stringfield Widget 248 | 249 | Args: 250 | label (str, optional): Label to the left of the UI element. Defaults to "". 251 | type (str, optional): Type of UI element. Defaults to "stringfield". 252 | default_val (str, optional): Text to initialize in Stringfield. Defaults to " ". 253 | tooltip (str, optional): Tooltip to display over the UI elements. Defaults to "". 254 | use_folder_picker (bool, optional): Add a folder picker button to the right. Defaults to False. 255 | read_only (bool, optional): Prevents editing. Defaults to False. 256 | item_filter_fn (Callable, optional): filter function to pass to the FilePicker 257 | bookmark_label (str, optional): bookmark label to pass to the FilePicker 258 | bookmark_path (str, optional): bookmark path to pass to the FilePicker 259 | Returns: 260 | AbstractValueModel: model of Stringfield 261 | """ 262 | with ui.HStack(): 263 | ui.Label(label, width=LABEL_WIDTH, alignment=ui.Alignment.LEFT_CENTER, tooltip=format_tt(tooltip)) 264 | str_field = ui.StringField( 265 | name="StringField", width=ui.Fraction(1), height=0, alignment=ui.Alignment.LEFT_CENTER, read_only=read_only 266 | ).model 267 | str_field.set_value(default_val) 268 | 269 | if use_folder_picker: 270 | 271 | def update_field(filename, path): 272 | if filename == "": 273 | val = path 274 | elif filename[0] != "/" and path[-1] != "/": 275 | val = path + "/" + filename 276 | elif filename[0] == "/" and path[-1] == "/": 277 | val = path + filename[1:] 278 | else: 279 | val = path + filename 280 | str_field.set_value(val) 281 | 282 | add_folder_picker_icon( 283 | update_field, 284 | item_filter_fn, 285 | bookmark_label, 286 | bookmark_path, 287 | dialog_title=folder_dialog_title, 288 | button_title=folder_button_title, 289 | ) 290 | else: 291 | add_line_rect_flourish(False) 292 | return str_field 293 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2022 NVIDIA CORPORATION 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | https://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /lightwheel/MJCF2USD/connection/option_widget.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2018-2023, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # NVIDIA CORPORATION and its licensors retain all intellectual property 4 | # and proprietary rights in and to this software, related documentation 5 | # and any modifications thereto. Any use, reproduction, disclosure or 6 | # distribution of this software and related documentation without an express 7 | # license agreement from NVIDIA CORPORATION is strictly prohibited. 8 | # 9 | import omni.ui as ui 10 | 11 | from .style import get_option_style 12 | from .ui_utils import add_folder_picker_icon, format_tt 13 | 14 | 15 | def checkbox_builder(label="", type="checkbox", default_val=False, tooltip="", on_clicked_fn=None): 16 | """Creates a Stylized Checkbox 17 | 18 | Args: 19 | label (str, optional): Label to the left of the UI element. Defaults to "". 20 | type (str, optional): Type of UI element. Defaults to "checkbox". 21 | default_val (bool, optional): Checked is True, Unchecked is False. Defaults to False. 22 | tooltip (str, optional): Tooltip to display over the Label. Defaults to "". 23 | on_clicked_fn (Callable, optional): Call-back function when clicked. Defaults to None. 24 | 25 | Returns: 26 | ui.SimpleBoolModel: model 27 | """ 28 | 29 | with ui.HStack(): 30 | check_box = ui.CheckBox(width=10, height=0) 31 | ui.Spacer(width=8) 32 | check_box.model.set_value(default_val) 33 | 34 | def on_click(value_model): 35 | on_clicked_fn(value_model.get_value_as_bool()) 36 | 37 | if on_clicked_fn: 38 | check_box.model.add_value_changed_fn(on_click) 39 | ui.Label(label, width=0, height=0, tooltip=tooltip) 40 | return check_box.model 41 | 42 | 43 | def float_field_builder(label="", default_val=0, tooltip="", format="%.2f"): 44 | """Creates a Stylized Floatfield Widget 45 | 46 | Args: 47 | label (str, optional): Label to the left of the UI element. Defaults to "". 48 | default_val (int, optional): Default Value of UI element. Defaults to 0. 49 | tooltip (str, optional): Tooltip to display over the UI elements. Defaults to "". 50 | 51 | Returns: 52 | AbstractValueModel: model 53 | """ 54 | with ui.HStack(spacing=10, style=get_option_style()): 55 | ui.Label(label, width=ui.Fraction(0.5), alignment=ui.Alignment.LEFT_CENTER, tooltip=format_tt(tooltip)) 56 | with ui.ZStack(): 57 | float_field = ui.FloatDrag( 58 | name="FloatDrag", 59 | width=ui.Fraction(0), 60 | height=0, 61 | alignment=ui.Alignment.LEFT, 62 | format=format, 63 | min=0, 64 | ).model 65 | float_field.set_value(default_val) 66 | with ui.HStack(): 67 | ui.Spacer() 68 | ui.Label("Kg/m", name="density", alignment=ui.Alignment.RIGHT_CENTER) 69 | ui.Label("3", name="exponent", alignment=ui.Alignment.RIGHT_TOP, width=0) 70 | ui.Spacer(width=1) 71 | return float_field 72 | 73 | 74 | def string_filed_builder( 75 | default_val=" ", 76 | tooltip="", 77 | read_only=False, 78 | item_filter_fn=None, 79 | folder_dialog_title="Select Output Folder", 80 | folder_button_title="Select Folder", 81 | ): 82 | """Creates a Stylized Stringfield Widget 83 | 84 | Args: 85 | default_val (str, optional): Text to initialize in Stringfield. Defaults to " ". 86 | tooltip (str, optional): Tooltip to display over the UI elements. Defaults to "". 87 | read_only (bool, optional): Prevents editing. Defaults to False. 88 | item_filter_fn (Callable, optional): filter function to pass to the FilePicker 89 | bookmark_label (str, optional): bookmark label to pass to the FilePicker 90 | bookmark_path (str, optional): bookmark path to pass to the FilePicker 91 | Returns: 92 | AbstractValueModel: model of Stringfield 93 | """ 94 | with ui.HStack(): 95 | ui.Spacer(width=4) 96 | str_field = ui.StringField( 97 | name="StringField", 98 | tooltip=format_tt(tooltip), 99 | width=ui.Fraction(1), 100 | height=0, 101 | alignment=ui.Alignment.LEFT_CENTER, 102 | read_only=read_only, 103 | ) 104 | str_field.enabled = True 105 | str_field.model.set_value(default_val) 106 | 107 | def update_field(filename, path): 108 | if filename == "": 109 | val = path 110 | elif filename[0] != "/" and path[-1] != "/": 111 | val = path + "/" + filename 112 | elif filename[0] == "/" and path[-1] == "/": 113 | val = path + filename[1:] 114 | else: 115 | val = path + filename 116 | str_field.model.set_value(val) 117 | 118 | ui.Spacer(width=4) 119 | file_pick_fn = add_folder_picker_icon( 120 | update_field, item_filter_fn, dialog_title=folder_dialog_title, button_title=folder_button_title, size=25) 121 | ui.Spacer(width=2) 122 | str_field.set_mouse_pressed_fn(lambda a, b, c, d: file_pick_fn()) 123 | return str_field.model 124 | 125 | 126 | def option_header(collapsed, title): 127 | with ui.HStack(height=22): 128 | ui.Spacer(width=4) 129 | with ui.VStack(width=10): 130 | ui.Spacer() 131 | if collapsed: 132 | triangle = ui.Triangle(height=7, width=5) 133 | triangle.alignment = ui.Alignment.RIGHT_CENTER 134 | else: 135 | triangle = ui.Triangle(height=5, width=7) 136 | triangle.alignment = ui.Alignment.CENTER_BOTTOM 137 | ui.Spacer() 138 | ui.Spacer(width=4) 139 | ui.Label(title, name="collapsable_header", width=0) 140 | ui.Spacer(width=3) 141 | ui.Line() 142 | 143 | 144 | def option_frame(title, build_content_fn, collapse_fn=None): 145 | with ui.CollapsableFrame( 146 | title, name="option", height=0, collapsed=False, build_header_fn=option_header, collapsed_changed_fn=collapse_fn 147 | ): 148 | with ui.HStack(): 149 | ui.Spacer(width=2) 150 | build_content_fn() 151 | ui.Spacer(width=2) 152 | 153 | 154 | class OptionWidget: 155 | def __init__(self, models, config): 156 | self._models = models 157 | self._config = config 158 | 159 | @property 160 | def models(self): 161 | return self._models 162 | 163 | @property 164 | def config(self): 165 | return self._config 166 | 167 | def build_options(self): 168 | with ui.VStack(style=get_option_style()): 169 | self._build_model_frame() 170 | self._build_links_frame() 171 | self._build_colliders_frame() 172 | 173 | def _build_model_frame(self): 174 | def build_model_content(): 175 | with ui.VStack(spacing=4): 176 | with ui.HStack(height=26): 177 | self._import_collection = ui.RadioCollection() 178 | with ui.VStack(width=0, alignment=ui.Alignment.LEFT_CENTER): 179 | ui.Spacer() 180 | ui.RadioButton( 181 | width=20, 182 | height=20, 183 | radio_collection=self._import_collection, 184 | alignment=ui.Alignment.LEFT_CENTER, 185 | ) 186 | ui.Spacer() 187 | ui.Spacer(width=4) 188 | ui.Label("Create in Stage", width=90) 189 | ui.Spacer(width=10) 190 | with ui.VStack(width=0): 191 | ui.Spacer() 192 | ui.RadioButton( 193 | width=20, 194 | height=20, 195 | radio_collection=self._import_collection, 196 | alignment=ui.Alignment.LEFT_CENTER, 197 | ) 198 | ui.Spacer() 199 | ui.Spacer(width=4) 200 | ui.Label("Referenced Model") 201 | ui.Spacer(width=50) 202 | self._import_collection.model.set_value(1) 203 | self._import_collection.model.add_value_changed_fn(lambda m: self._update_import_option(m)) 204 | self._models["import_as_reference"] = True 205 | self._add_as_reference_frame = ui.VStack() 206 | with self._add_as_reference_frame: 207 | ui.Label("USD Output") 208 | self._models["instanceable_usd_path"] = string_filed_builder( 209 | tooltip="USD file to store instanceable meshes in", 210 | default_val="Same as Imported Model(Default)", 211 | folder_dialog_title="Select Output File", 212 | folder_button_title="Select File", 213 | read_only=True, 214 | ) 215 | self._add_to_stage_frame = ui.VStack() 216 | with self._add_to_stage_frame: 217 | # TODO: when change this to False, will raise a 'not found default prim' error in _load_robot 218 | checkbox_builder( 219 | "Set as Default Prim", 220 | tooltip="If true, makes imported robot the default prim for the stage", 221 | default_val=self._config.make_default_prim, 222 | on_clicked_fn=lambda m, config=self._config: config.set_make_default_prim(m), 223 | ) 224 | 225 | self._models["clean_stage"] = checkbox_builder( 226 | "Clear Stage on Import", 227 | tooltip="Check this box to load URDF on a clean stage", 228 | default_val=False, 229 | ) 230 | self._add_to_stage_frame.visible = False 231 | checkbox_builder( 232 | "Import Sites", 233 | tooltip="If True, sites will be imported from mjcf.", 234 | default_val=True, 235 | on_clicked_fn=lambda m, config=self._config: config.set_import_sites(m), 236 | ) 237 | 238 | option_frame("Model", build_model_content) 239 | 240 | def _build_links_frame(self): 241 | def build_links_content(): 242 | with ui.VStack(spacing=4): 243 | with ui.HStack(height=24): 244 | ui.Spacer(width=0) 245 | self._base_collection = ui.RadioCollection() 246 | with ui.VStack(width=0): 247 | ui.Spacer() 248 | ui.RadioButton(width=20, height=20, radio_collection=self._base_collection) 249 | ui.Spacer() 250 | ui.Spacer(width=4) 251 | ui.Label("Moveable Base", width=90) 252 | ui.Spacer(width=10) 253 | with ui.VStack(width=0): 254 | ui.Spacer() 255 | ui.RadioButton(width=20, height=20, radio_collection=self._base_collection) 256 | ui.Spacer() 257 | ui.Spacer(width=4) 258 | ui.Label("Static Base") 259 | index = 1 if self._config.fix_base else 0 260 | self._base_collection.model.set_value(index) 261 | self._base_collection.model.add_value_changed_fn(lambda m: self._update_fix_base(m)) 262 | self._models["density"] = float_field_builder( 263 | "Default Density", 264 | default_val=self._config.density, 265 | tooltip="[kg/stage_units^3] If a link doesn't have mass, use this density as backup, A density of 0.0 results in the physics engine automatically computing a default density", 266 | ) 267 | self._models["density"].add_value_changed_fn( 268 | lambda m, config=self._config: config.set_density(m.get_value_as_float()) 269 | ) 270 | 271 | option_frame("Links", build_links_content) 272 | 273 | def _update_fix_base(self, model): 274 | value = model.get_value_as_bool() 275 | self._config.set_fix_base(value) 276 | self._config.set_visualize_collision_geoms(False) 277 | 278 | def _build_colliders_frame(self): 279 | def build_colliders_content(): 280 | with ui.VStack(spacing=4): 281 | checkbox_builder( 282 | "Visualize Collision Geometry", 283 | tooltip="If True, collision geoms will also be imported as visual geoms", 284 | default_val=False, 285 | on_clicked_fn=lambda m, config=self._config: config.set_visualize_collision_geoms(m), 286 | ) 287 | 288 | checkbox_builder( 289 | "Self Collision", 290 | tooltip="If true, allows self intersection between links in the robot, can cause instability if collision meshes between links are self intersecting", 291 | default_val=self._config.self_collision, 292 | on_clicked_fn=lambda m, config=self._config: config.set_self_collision(m), 293 | ) 294 | 295 | option_frame("Colliders", build_colliders_content) 296 | 297 | def _update_import_option(self, model): 298 | value = bool(model.get_value_as_int() == 1) 299 | self._add_as_reference_frame.visible = value 300 | self._add_to_stage_frame.visible = not value 301 | self._models["import_as_reference"] = value 302 | -------------------------------------------------------------------------------- /lightwheel/MJCF2USD/connection/style.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. 2 | # 3 | # NVIDIA CORPORATION and its licensors retain all intellectual property 4 | # and proprietary rights in and to this software, related documentation 5 | # and any modifications thereto. Any use, reproduction, disclosure or 6 | # distribution of this software and related documentation without an express 7 | # license agreement from NVIDIA CORPORATION is strictly prohibited. 8 | # 9 | 10 | import pathlib 11 | 12 | import carb.settings 13 | import omni.kit.app 14 | import omni.ui as ui 15 | from omni.kit.window.extensions.common import get_icons_path 16 | from omni.ui import color as cl 17 | 18 | # Pilaged from omni.kit.widnow.property style.py 19 | 20 | LABEL_WIDTH = 120 21 | BUTTON_WIDTH = 120 22 | HORIZONTAL_SPACING = 4 23 | VERTICAL_SPACING = 5 24 | COLOR_X = 0xFF5555AA 25 | COLOR_Y = 0xFF76A371 26 | COLOR_Z = 0xFFA07D4F 27 | COLOR_W = 0xFFAA5555 28 | 29 | 30 | def get_style(): 31 | 32 | icons_path = get_icons_path() 33 | 34 | KIT_GREEN = 0xFF8A8777 35 | KIT_GREEN_CHECKBOX = 0xFF9A9A9A 36 | BORDER_RADIUS = 1.5 37 | FONT_SIZE = 14.0 38 | TOOLTIP_STYLE = ( 39 | { 40 | "background_color": 0xFFD1F7FF, 41 | "color": 0xFF333333, 42 | "margin_width": 0, 43 | "margin_height": 0, 44 | "padding": 0, 45 | "border_width": 0, 46 | "border_radius": 1.5, 47 | "border_color": 0x0, 48 | }, 49 | ) 50 | 51 | style_settings = carb.settings.get_settings().get("/persistent/app/window/uiStyle") 52 | if not style_settings: 53 | style_settings = "NvidiaDark" 54 | 55 | if style_settings == "NvidiaLight": 56 | WINDOW_BACKGROUND_COLOR = 0xFF444444 57 | BUTTON_BACKGROUND_COLOR = 0xFF545454 58 | BUTTON_BACKGROUND_HOVERED_COLOR = 0xFF9E9E9E 59 | BUTTON_BACKGROUND_PRESSED_COLOR = 0xC22A8778 60 | BUTTON_LABEL_DISABLED_COLOR = 0xFF606060 61 | 62 | FRAME_TEXT_COLOR = 0xFF545454 63 | FIELD_BACKGROUND = 0xFF545454 64 | FIELD_SECONDARY = 0xFFABABAB 65 | FIELD_TEXT_COLOR = 0xFFD6D6D6 66 | FIELD_TEXT_COLOR_READ_ONLY = 0xFF9C9C9C 67 | FIELD_TEXT_COLOR_HIDDEN = 0x01000000 68 | COLLAPSABLEFRAME_BORDER_COLOR = 0x0 69 | COLLAPSABLEFRAME_BACKGROUND_COLOR = 0x7FD6D6D6 70 | COLLAPSABLEFRAME_TEXT_COLOR = 0xFF545454 71 | 72 | COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR = 0xFFC9C9C9 73 | COLLAPSABLEFRAME_SUBFRAME_BACKGROUND_COLOR = 0xFFD6D6D6 74 | COLLAPSABLEFRAME_HOVERED_BACKGROUND_COLOR = 0xFFCCCFBF 75 | COLLAPSABLEFRAME_PRESSED_BACKGROUND_COLOR = 0xFF2E2E2B 76 | COLLAPSABLEFRAME_HOVERED_SECONDARY_COLOR = 0xFFD6D6D6 77 | COLLAPSABLEFRAME_PRESSED_SECONDARY_COLOR = 0xFFE6E6E6 78 | LABEL_VECTORLABEL_COLOR = 0xFFDDDDDD 79 | LABEL_MIXED_COLOR = 0xFFD6D6D6 80 | LIGHT_FONT_SIZE = 14.0 81 | LIGHT_BORDER_RADIUS = 3 82 | 83 | style = { 84 | "Window": {"background_color": WINDOW_BACKGROUND_COLOR}, 85 | "Button": {"background_color": BUTTON_BACKGROUND_COLOR, "margin": 0, "padding": 3, "border_radius": 2}, 86 | "Button:hovered": {"background_color": BUTTON_BACKGROUND_HOVERED_COLOR}, 87 | "Button:pressed": {"background_color": BUTTON_BACKGROUND_PRESSED_COLOR}, 88 | "Button.Label:disabled": {"color": 0xFFD6D6D6}, 89 | "Button.Label": {"color": 0xFFD6D6D6}, 90 | "Field::models": { 91 | "background_color": FIELD_BACKGROUND, 92 | "font_size": LIGHT_FONT_SIZE, 93 | "color": FIELD_TEXT_COLOR, 94 | "border_radius": LIGHT_BORDER_RADIUS, 95 | "secondary_color": FIELD_SECONDARY, 96 | }, 97 | "Field::models_mixed": { 98 | "background_color": FIELD_BACKGROUND, 99 | "font_size": LIGHT_FONT_SIZE, 100 | "color": FIELD_TEXT_COLOR_HIDDEN, 101 | "border_radius": LIGHT_BORDER_RADIUS, 102 | }, 103 | "Field::models_readonly": { 104 | "background_color": FIELD_BACKGROUND, 105 | "font_size": LIGHT_FONT_SIZE, 106 | "color": FIELD_TEXT_COLOR_READ_ONLY, 107 | "border_radius": LIGHT_BORDER_RADIUS, 108 | "secondary_color": FIELD_SECONDARY, 109 | }, 110 | "Field::models_readonly_mixed": { 111 | "background_color": FIELD_BACKGROUND, 112 | "font_size": LIGHT_FONT_SIZE, 113 | "color": FIELD_TEXT_COLOR_HIDDEN, 114 | "border_radius": LIGHT_BORDER_RADIUS, 115 | }, 116 | "Field::models:pressed": {"background_color": 0xFFCECECE}, 117 | "Field": {"background_color": 0xFF535354, "color": 0xFFCCCCCC}, 118 | "Label": {"font_size": 12, "color": FRAME_TEXT_COLOR}, 119 | "Label::label:disabled": {"color": BUTTON_LABEL_DISABLED_COLOR}, 120 | "Label::label": { 121 | "font_size": LIGHT_FONT_SIZE, 122 | "background_color": FIELD_BACKGROUND, 123 | "color": FRAME_TEXT_COLOR, 124 | }, 125 | "Label::title": { 126 | "font_size": LIGHT_FONT_SIZE, 127 | "background_color": FIELD_BACKGROUND, 128 | "color": FRAME_TEXT_COLOR, 129 | }, 130 | "Label::mixed_overlay": { 131 | "font_size": LIGHT_FONT_SIZE, 132 | "background_color": FIELD_BACKGROUND, 133 | "color": FRAME_TEXT_COLOR, 134 | }, 135 | "Label::mixed_overlay_normal": { 136 | "font_size": LIGHT_FONT_SIZE, 137 | "background_color": FIELD_BACKGROUND, 138 | "color": FRAME_TEXT_COLOR, 139 | }, 140 | "ComboBox::choices": { 141 | "font_size": 12, 142 | "color": 0xFFD6D6D6, 143 | "background_color": FIELD_BACKGROUND, 144 | "secondary_color": FIELD_BACKGROUND, 145 | "border_radius": LIGHT_BORDER_RADIUS * 2, 146 | }, 147 | "ComboBox::xform_op": { 148 | "font_size": 10, 149 | "color": 0xFF333333, 150 | "background_color": 0xFF9C9C9C, 151 | "secondary_color": 0x0, 152 | "selected_color": 0xFFACACAF, 153 | "border_radius": LIGHT_BORDER_RADIUS * 2, 154 | }, 155 | "ComboBox::xform_op:hovered": {"background_color": 0x0}, 156 | "ComboBox::xform_op:selected": {"background_color": 0xFF545454}, 157 | "ComboBox": { 158 | "font_size": 10, 159 | "color": 0xFFE6E6E6, 160 | "background_color": 0xFF545454, 161 | "secondary_color": 0xFF545454, 162 | "selected_color": 0xFFACACAF, 163 | "border_radius": LIGHT_BORDER_RADIUS * 2, 164 | }, 165 | # "ComboBox": {"background_color": 0xFF535354, "selected_color": 0xFFACACAF, "color": 0xFFD6D6D6}, 166 | "ComboBox:hovered": {"background_color": 0xFF545454}, 167 | "ComboBox:selected": {"background_color": 0xFF545454}, 168 | "ComboBox::choices_mixed": { 169 | "font_size": LIGHT_FONT_SIZE, 170 | "color": 0xFFD6D6D6, 171 | "background_color": FIELD_BACKGROUND, 172 | "secondary_color": FIELD_BACKGROUND, 173 | "secondary_selected_color": FIELD_TEXT_COLOR, 174 | "border_radius": LIGHT_BORDER_RADIUS * 2, 175 | }, 176 | "ComboBox:hovered:choices": {"background_color": FIELD_BACKGROUND, "secondary_color": FIELD_BACKGROUND}, 177 | "Slider": { 178 | "font_size": LIGHT_FONT_SIZE, 179 | "color": FIELD_TEXT_COLOR, 180 | "border_radius": LIGHT_BORDER_RADIUS, 181 | "background_color": FIELD_BACKGROUND, 182 | "secondary_color": WINDOW_BACKGROUND_COLOR, 183 | "draw_mode": ui.SliderDrawMode.FILLED, 184 | }, 185 | "Slider::value": { 186 | "font_size": LIGHT_FONT_SIZE, 187 | "color": FIELD_TEXT_COLOR, # COLLAPSABLEFRAME_TEXT_COLOR 188 | "border_radius": LIGHT_BORDER_RADIUS, 189 | "background_color": FIELD_BACKGROUND, 190 | "secondary_color": KIT_GREEN, 191 | }, 192 | "Slider::value_mixed": { 193 | "font_size": LIGHT_FONT_SIZE, 194 | "color": FIELD_TEXT_COLOR_HIDDEN, 195 | "border_radius": LIGHT_BORDER_RADIUS, 196 | "background_color": FIELD_BACKGROUND, 197 | "secondary_color": KIT_GREEN, 198 | }, 199 | "Slider::multivalue": { 200 | "font_size": LIGHT_FONT_SIZE, 201 | "color": FIELD_TEXT_COLOR, 202 | "border_radius": LIGHT_BORDER_RADIUS, 203 | "background_color": FIELD_BACKGROUND, 204 | "secondary_color": KIT_GREEN, 205 | "draw_mode": ui.SliderDrawMode.HANDLE, 206 | }, 207 | "Slider::multivalue_mixed": { 208 | "font_size": LIGHT_FONT_SIZE, 209 | "color": FIELD_TEXT_COLOR_HIDDEN, 210 | "border_radius": LIGHT_BORDER_RADIUS, 211 | "background_color": FIELD_BACKGROUND, 212 | "secondary_color": KIT_GREEN, 213 | "draw_mode": ui.SliderDrawMode.HANDLE, 214 | }, 215 | "Checkbox": { 216 | "margin": 0, 217 | "padding": 0, 218 | "radius": 0, 219 | "font_size": 10, 220 | "background_color": 0xFFA8A8A8, 221 | "background_color": 0xFFA8A8A8, 222 | }, 223 | "CheckBox::greenCheck": {"font_size": 10, "background_color": KIT_GREEN, "color": 0xFF23211F}, 224 | "CheckBox::greenCheck_mixed": { 225 | "font_size": 10, 226 | "background_color": KIT_GREEN, 227 | "color": FIELD_TEXT_COLOR_HIDDEN, 228 | "border_radius": LIGHT_BORDER_RADIUS, 229 | }, 230 | "CollapsableFrame": { 231 | "background_color": COLLAPSABLEFRAME_BACKGROUND_COLOR, 232 | "secondary_color": COLLAPSABLEFRAME_BACKGROUND_COLOR, 233 | "color": COLLAPSABLEFRAME_TEXT_COLOR, 234 | "border_radius": LIGHT_BORDER_RADIUS, 235 | "border_color": 0x0, 236 | "border_width": 1, 237 | "font_size": LIGHT_FONT_SIZE, 238 | "padding": 6, 239 | "Tooltip": TOOLTIP_STYLE, 240 | }, 241 | "CollapsableFrame::groupFrame": { 242 | "background_color": COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR, 243 | "secondary_color": COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR, 244 | "border_radius": BORDER_RADIUS * 2, 245 | "padding": 6, 246 | }, 247 | "CollapsableFrame::groupFrame:hovered": { 248 | "background_color": COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR, 249 | "secondary_color": COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR, 250 | }, 251 | "CollapsableFrame::groupFrame:pressed": { 252 | "background_color": COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR, 253 | "secondary_color": COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR, 254 | }, 255 | "CollapsableFrame::subFrame": { 256 | "background_color": COLLAPSABLEFRAME_SUBFRAME_BACKGROUND_COLOR, 257 | "secondary_color": COLLAPSABLEFRAME_SUBFRAME_BACKGROUND_COLOR, 258 | }, 259 | "CollapsableFrame::subFrame:hovered": { 260 | "background_color": COLLAPSABLEFRAME_SUBFRAME_BACKGROUND_COLOR, 261 | "secondary_color": COLLAPSABLEFRAME_HOVERED_BACKGROUND_COLOR, 262 | }, 263 | "CollapsableFrame::subFrame:pressed": { 264 | "background_color": COLLAPSABLEFRAME_SUBFRAME_BACKGROUND_COLOR, 265 | "secondary_color": COLLAPSABLEFRAME_PRESSED_BACKGROUND_COLOR, 266 | }, 267 | "CollapsableFrame.Header": { 268 | "font_size": LIGHT_FONT_SIZE, 269 | "background_color": FRAME_TEXT_COLOR, 270 | "color": FRAME_TEXT_COLOR, 271 | }, 272 | "CollapsableFrame:hovered": {"secondary_color": COLLAPSABLEFRAME_HOVERED_SECONDARY_COLOR}, 273 | "CollapsableFrame:pressed": {"secondary_color": COLLAPSABLEFRAME_PRESSED_SECONDARY_COLOR}, 274 | "ScrollingFrame": {"margin": 0, "padding": 3, "border_radius": LIGHT_BORDER_RADIUS}, 275 | "TreeView": { 276 | "background_color": 0xFFE0E0E0, 277 | "background_selected_color": 0x109D905C, 278 | "secondary_color": 0xFFACACAC, 279 | }, 280 | "TreeView.ScrollingFrame": {"background_color": 0xFFE0E0E0}, 281 | "TreeView.Header": {"color": 0xFFCCCCCC}, 282 | "TreeView.Header::background": { 283 | "background_color": 0xFF535354, 284 | "border_color": 0xFF707070, 285 | "border_width": 0.5, 286 | }, 287 | "TreeView.Header::columnname": {"margin": 3}, 288 | "TreeView.Image::object_icon_grey": {"color": 0x80FFFFFF}, 289 | "TreeView.Item": {"color": 0xFF535354, "font_size": 16}, 290 | "TreeView.Item::object_name": {"margin": 3}, 291 | "TreeView.Item::object_name_grey": {"color": 0xFFACACAC}, 292 | "TreeView.Item::object_name_missing": {"color": 0xFF6F72FF}, 293 | "TreeView.Item:selected": {"color": 0xFF2A2825}, 294 | "TreeView:selected": {"background_color": 0x409D905C}, 295 | "Label::vector_label": {"font_size": 14, "color": LABEL_VECTORLABEL_COLOR}, 296 | "Rectangle::vector_label": {"border_radius": BORDER_RADIUS * 2, "corner_flag": ui.CornerFlag.LEFT}, 297 | "Rectangle::mixed_overlay": { 298 | "border_radius": LIGHT_BORDER_RADIUS, 299 | "background_color": FIELD_BACKGROUND, 300 | "border_width": 3, 301 | }, 302 | "Rectangle": { 303 | "border_radius": LIGHT_BORDER_RADIUS, 304 | "color": 0xFFC2C2C2, 305 | "background_color": 0xFFC2C2C2, 306 | }, # FIELD_BACKGROUND}, 307 | "Rectangle::xform_op:hovered": {"background_color": 0x0}, 308 | "Rectangle::xform_op": {"background_color": 0x0}, 309 | # text remove 310 | "Button::remove": {"background_color": FIELD_BACKGROUND, "margin": 0}, 311 | "Button::remove:hovered": {"background_color": FIELD_BACKGROUND}, 312 | "Button::options": {"background_color": 0x0, "margin": 0}, 313 | "Button.Image::options": {"image_url": f"{icons_path}/options.svg", "color": 0xFF989898}, 314 | "Button.Image::options:hovered": {"color": 0xFFC2C2C2}, 315 | "IconButton": {"margin": 0, "padding": 0, "background_color": 0x0}, 316 | "IconButton:hovered": {"background_color": 0x0}, 317 | "IconButton:checked": {"background_color": 0x0}, 318 | "IconButton:pressed": {"background_color": 0x0}, 319 | "IconButton.Image": {"color": 0xFFA8A8A8}, 320 | "IconButton.Image:hovered": {"color": 0xFF929292}, 321 | "IconButton.Image:pressed": {"color": 0xFFA4A4A4}, 322 | "IconButton.Image:checked": {"color": 0xFFFFFFFF}, 323 | "IconButton.Tooltip": {"color": 0xFF9E9E9E}, 324 | "IconButton.Image::OpenFolder": { 325 | "image_url": f"{icons_path}/open-folder.svg", 326 | "background_color": 0x0, 327 | "color": 0xFFA8A8A8, 328 | "tooltip": TOOLTIP_STYLE, 329 | }, 330 | "IconButton.Image::OpenConfig": { 331 | "image_url": f"{icons_path}/open-config.svg", 332 | "background_color": 0x0, 333 | "color": 0xFFA8A8A8, 334 | "tooltip": TOOLTIP_STYLE, 335 | }, 336 | "IconButton.Image::OpenLink": { 337 | "image_url": "resources/glyphs/link.svg", 338 | "background_color": 0x0, 339 | "color": 0xFFA8A8A8, 340 | "tooltip": TOOLTIP_STYLE, 341 | }, 342 | "IconButton.Image::OpenDocs": { 343 | "image_url": "resources/glyphs/docs.svg", 344 | "background_color": 0x0, 345 | "color": 0xFFA8A8A8, 346 | "tooltip": TOOLTIP_STYLE, 347 | }, 348 | "IconButton.Image::CopyToClipboard": { 349 | "image_url": "resources/glyphs/copy.svg", 350 | "background_color": 0x0, 351 | "color": 0xFFA8A8A8, 352 | }, 353 | "IconButton.Image::Export": { 354 | "image_url": f"{icons_path}/export.svg", 355 | "background_color": 0x0, 356 | "color": 0xFFA8A8A8, 357 | }, 358 | "IconButton.Image::Sync": { 359 | "image_url": "resources/glyphs/sync.svg", 360 | "background_color": 0x0, 361 | "color": 0xFFA8A8A8, 362 | }, 363 | "IconButton.Image::Upload": { 364 | "image_url": "resources/glyphs/upload.svg", 365 | "background_color": 0x0, 366 | "color": 0xFFA8A8A8, 367 | }, 368 | "IconButton.Image::FolderPicker": { 369 | "image_url": "resources/glyphs/folder.svg", 370 | "background_color": 0x0, 371 | "color": 0xFFA8A8A8, 372 | }, 373 | "ItemButton": {"padding": 2, "background_color": 0xFF444444, "border_radius": 4}, 374 | "ItemButton.Image::add": {"image_url": f"{icons_path}/plus.svg", "color": 0xFF06C66B}, 375 | "ItemButton.Image::remove": {"image_url": f"{icons_path}/trash.svg", "color": 0xFF1010C6}, 376 | "ItemButton:hovered": {"background_color": 0xFF333333}, 377 | "ItemButton:pressed": {"background_color": 0xFF222222}, 378 | "Tooltip": TOOLTIP_STYLE, 379 | } 380 | else: 381 | LABEL_COLOR = 0xFF8F8E86 382 | FIELD_BACKGROUND = 0xFF23211F 383 | FIELD_TEXT_COLOR = 0xFFD5D5D5 384 | FIELD_TEXT_COLOR_READ_ONLY = 0xFF5C5C5C 385 | FIELD_TEXT_COLOR_HIDDEN = 0x01000000 386 | FRAME_TEXT_COLOR = 0xFFCCCCCC 387 | WINDOW_BACKGROUND_COLOR = 0xFF444444 388 | BUTTON_BACKGROUND_COLOR = 0xFF292929 389 | BUTTON_BACKGROUND_HOVERED_COLOR = 0xFF9E9E9E 390 | BUTTON_BACKGROUND_PRESSED_COLOR = 0xC22A8778 391 | BUTTON_LABEL_DISABLED_COLOR = 0xFF606060 392 | LABEL_LABEL_COLOR = 0xFF9E9E9E 393 | LABEL_TITLE_COLOR = 0xFFAAAAAA 394 | LABEL_MIXED_COLOR = 0xFFE6B067 395 | LABEL_VECTORLABEL_COLOR = 0xFFDDDDDD 396 | COLORWIDGET_BORDER_COLOR = 0xFF1E1E1E 397 | COMBOBOX_HOVERED_BACKGROUND_COLOR = 0xFF33312F 398 | COLLAPSABLEFRAME_BORDER_COLOR = 0x0 399 | COLLAPSABLEFRAME_BACKGROUND_COLOR = 0xFF343432 400 | COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR = 0xFF23211F 401 | COLLAPSABLEFRAME_SUBFRAME_BACKGROUND_COLOR = 0xFF343432 402 | COLLAPSABLEFRAME_HOVERED_BACKGROUND_COLOR = 0xFF2E2E2B 403 | COLLAPSABLEFRAME_PRESSED_BACKGROUND_COLOR = 0xFF2E2E2B 404 | 405 | style = { 406 | "Window": {"background_color": WINDOW_BACKGROUND_COLOR}, 407 | "Button": {"background_color": BUTTON_BACKGROUND_COLOR, "margin": 0, "padding": 3, "border_radius": 2}, 408 | "Button:hovered": {"background_color": BUTTON_BACKGROUND_HOVERED_COLOR}, 409 | "Button:pressed": {"background_color": BUTTON_BACKGROUND_PRESSED_COLOR}, 410 | "Button.Label:disabled": {"color": BUTTON_LABEL_DISABLED_COLOR}, 411 | "Field::models": { 412 | "background_color": FIELD_BACKGROUND, 413 | "font_size": FONT_SIZE, 414 | "color": FIELD_TEXT_COLOR, 415 | "border_radius": BORDER_RADIUS, 416 | }, 417 | "Field::models_mixed": { 418 | "background_color": FIELD_BACKGROUND, 419 | "font_size": FONT_SIZE, 420 | "color": FIELD_TEXT_COLOR_HIDDEN, 421 | "border_radius": BORDER_RADIUS, 422 | }, 423 | "Field::models_readonly": { 424 | "background_color": FIELD_BACKGROUND, 425 | "font_size": FONT_SIZE, 426 | "color": FIELD_TEXT_COLOR_READ_ONLY, 427 | "border_radius": BORDER_RADIUS, 428 | }, 429 | "Field::models_readonly_mixed": { 430 | "background_color": FIELD_BACKGROUND, 431 | "font_size": FONT_SIZE, 432 | "color": FIELD_TEXT_COLOR_HIDDEN, 433 | "border_radius": BORDER_RADIUS, 434 | }, 435 | "Label": {"font_size": FONT_SIZE, "color": LABEL_COLOR}, 436 | "Label::label": {"font_size": FONT_SIZE, "color": LABEL_LABEL_COLOR}, 437 | "Label::label:disabled": {"color": BUTTON_LABEL_DISABLED_COLOR}, 438 | "Label::title": {"font_size": FONT_SIZE, "color": LABEL_TITLE_COLOR}, 439 | "Label::mixed_overlay": {"font_size": FONT_SIZE, "color": LABEL_MIXED_COLOR}, 440 | "Label::mixed_overlay_normal": {"font_size": FONT_SIZE, "color": FIELD_TEXT_COLOR}, 441 | "Label::path_label": {"font_size": FONT_SIZE, "color": LABEL_LABEL_COLOR}, 442 | "Label::stage_label": {"font_size": FONT_SIZE, "color": LABEL_LABEL_COLOR}, 443 | "ComboBox::choices": { 444 | "font_size": FONT_SIZE, 445 | "color": FIELD_TEXT_COLOR, 446 | "background_color": FIELD_BACKGROUND, 447 | "secondary_color": FIELD_BACKGROUND, 448 | "secondary_selected_color": FIELD_TEXT_COLOR, 449 | "border_radius": BORDER_RADIUS, 450 | }, 451 | "ComboBox::choices_mixed": { 452 | "font_size": FONT_SIZE, 453 | "color": FIELD_TEXT_COLOR_HIDDEN, 454 | "background_color": FIELD_BACKGROUND, 455 | "secondary_color": FIELD_BACKGROUND, 456 | "secondary_selected_color": FIELD_TEXT_COLOR, 457 | "border_radius": BORDER_RADIUS, 458 | }, 459 | "ComboBox:hovered:choices": { 460 | "background_color": COMBOBOX_HOVERED_BACKGROUND_COLOR, 461 | "secondary_color": COMBOBOX_HOVERED_BACKGROUND_COLOR, 462 | }, 463 | "Slider": { 464 | "font_size": FONT_SIZE, 465 | "color": FIELD_TEXT_COLOR, 466 | "border_radius": BORDER_RADIUS, 467 | "background_color": FIELD_BACKGROUND, 468 | "secondary_color": WINDOW_BACKGROUND_COLOR, 469 | "draw_mode": ui.SliderDrawMode.FILLED, 470 | }, 471 | "Slider::value": { 472 | "font_size": FONT_SIZE, 473 | "color": FIELD_TEXT_COLOR, 474 | "border_radius": BORDER_RADIUS, 475 | "background_color": FIELD_BACKGROUND, 476 | "secondary_color": WINDOW_BACKGROUND_COLOR, 477 | }, 478 | "Slider::value_mixed": { 479 | "font_size": FONT_SIZE, 480 | "color": FIELD_TEXT_COLOR_HIDDEN, 481 | "border_radius": BORDER_RADIUS, 482 | "background_color": FIELD_BACKGROUND, 483 | "secondary_color": WINDOW_BACKGROUND_COLOR, 484 | }, 485 | "Slider::multivalue": { 486 | "font_size": FONT_SIZE, 487 | "color": FIELD_TEXT_COLOR, 488 | "border_radius": BORDER_RADIUS, 489 | "background_color": FIELD_BACKGROUND, 490 | "secondary_color": WINDOW_BACKGROUND_COLOR, 491 | "draw_mode": ui.SliderDrawMode.HANDLE, 492 | }, 493 | "Slider::multivalue_mixed": { 494 | "font_size": FONT_SIZE, 495 | "color": FIELD_TEXT_COLOR, 496 | "border_radius": BORDER_RADIUS, 497 | "background_color": FIELD_BACKGROUND, 498 | "secondary_color": WINDOW_BACKGROUND_COLOR, 499 | "draw_mode": ui.SliderDrawMode.HANDLE, 500 | }, 501 | "CheckBox::greenCheck": { 502 | "font_size": 12, 503 | "background_color": KIT_GREEN_CHECKBOX, 504 | "color": FIELD_BACKGROUND, 505 | "border_radius": BORDER_RADIUS, 506 | }, 507 | "CheckBox::greenCheck_mixed": { 508 | "font_size": 12, 509 | "background_color": KIT_GREEN_CHECKBOX, 510 | "color": FIELD_TEXT_COLOR_HIDDEN, 511 | "border_radius": BORDER_RADIUS, 512 | }, 513 | "CollapsableFrame": { 514 | "background_color": COLLAPSABLEFRAME_BACKGROUND_COLOR, 515 | "secondary_color": COLLAPSABLEFRAME_BACKGROUND_COLOR, 516 | "border_radius": BORDER_RADIUS * 2, 517 | "border_color": COLLAPSABLEFRAME_BORDER_COLOR, 518 | "border_width": 1, 519 | "padding": 6, 520 | }, 521 | "CollapsableFrame::groupFrame": { 522 | "background_color": COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR, 523 | "secondary_color": COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR, 524 | "border_radius": BORDER_RADIUS * 2, 525 | "padding": 6, 526 | }, 527 | "CollapsableFrame::groupFrame:hovered": { 528 | "background_color": COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR, 529 | "secondary_color": COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR, 530 | }, 531 | "CollapsableFrame::groupFrame:pressed": { 532 | "background_color": COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR, 533 | "secondary_color": COLLAPSABLEFRAME_GROUPFRAME_BACKGROUND_COLOR, 534 | }, 535 | "CollapsableFrame::subFrame": { 536 | "background_color": COLLAPSABLEFRAME_SUBFRAME_BACKGROUND_COLOR, 537 | "secondary_color": COLLAPSABLEFRAME_SUBFRAME_BACKGROUND_COLOR, 538 | }, 539 | "CollapsableFrame::subFrame:hovered": { 540 | "background_color": COLLAPSABLEFRAME_SUBFRAME_BACKGROUND_COLOR, 541 | "secondary_color": COLLAPSABLEFRAME_HOVERED_BACKGROUND_COLOR, 542 | }, 543 | "CollapsableFrame::subFrame:pressed": { 544 | "background_color": COLLAPSABLEFRAME_SUBFRAME_BACKGROUND_COLOR, 545 | "secondary_color": COLLAPSABLEFRAME_PRESSED_BACKGROUND_COLOR, 546 | }, 547 | "CollapsableFrame.Header": { 548 | "font_size": FONT_SIZE, 549 | "background_color": FRAME_TEXT_COLOR, 550 | "color": FRAME_TEXT_COLOR, 551 | }, 552 | "CollapsableFrame:hovered": {"secondary_color": COLLAPSABLEFRAME_HOVERED_BACKGROUND_COLOR}, 553 | "CollapsableFrame:pressed": {"secondary_color": COLLAPSABLEFRAME_PRESSED_BACKGROUND_COLOR}, 554 | "ScrollingFrame": {"margin": 0, "padding": 3, "border_radius": BORDER_RADIUS}, 555 | "TreeView": { 556 | "background_color": 0xFF23211F, 557 | "background_selected_color": 0x664F4D43, 558 | "secondary_color": 0xFF403B3B, 559 | }, 560 | "TreeView.ScrollingFrame": {"background_color": 0xFF23211F}, 561 | "TreeView.Header": {"background_color": 0xFF343432, "color": 0xFFCCCCCC, "font_size": 12}, 562 | "TreeView.Image::object_icon_grey": {"color": 0x80FFFFFF}, 563 | "TreeView.Image:disabled": {"color": 0x60FFFFFF}, 564 | "TreeView.Item": {"color": 0xFF8A8777}, 565 | "TreeView.Item:disabled": {"color": 0x608A8777}, 566 | "TreeView.Item::object_name_grey": {"color": 0xFF4D4B42}, 567 | "TreeView.Item::object_name_missing": {"color": 0xFF6F72FF}, 568 | "TreeView.Item:selected": {"color": 0xFF23211F}, 569 | "TreeView:selected": {"background_color": 0xFF8A8777}, 570 | "ColorWidget": { 571 | "border_radius": BORDER_RADIUS, 572 | "border_color": COLORWIDGET_BORDER_COLOR, 573 | "border_width": 0.5, 574 | }, 575 | "Label::vector_label": {"font_size": 16, "color": LABEL_VECTORLABEL_COLOR}, 576 | "PlotLabel::X": {"color": 0xFF1515EA, "background_color": 0x0}, 577 | "PlotLabel::Y": {"color": 0xFF5FC054, "background_color": 0x0}, 578 | "PlotLabel::Z": {"color": 0xFFC5822A, "background_color": 0x0}, 579 | "PlotLabel::W": {"color": 0xFFAA5555, "background_color": 0x0}, 580 | "Rectangle::vector_label": {"border_radius": BORDER_RADIUS * 2, "corner_flag": ui.CornerFlag.LEFT}, 581 | "Rectangle::mixed_overlay": { 582 | "border_radius": BORDER_RADIUS, 583 | "background_color": LABEL_MIXED_COLOR, 584 | "border_width": 3, 585 | }, 586 | "Rectangle": { 587 | "border_radius": BORDER_RADIUS, 588 | "background_color": FIELD_TEXT_COLOR_READ_ONLY, 589 | }, # FIELD_BACKGROUND}, 590 | "Rectangle::xform_op:hovered": {"background_color": 0xFF444444}, 591 | "Rectangle::xform_op": {"background_color": 0xFF333333}, 592 | # text remove 593 | "Button::remove": {"background_color": FIELD_BACKGROUND, "margin": 0}, 594 | "Button::remove:hovered": {"background_color": FIELD_BACKGROUND}, 595 | "Button::options": {"background_color": 0x0, "margin": 0}, 596 | "Button.Image::options": {"image_url": f"{icons_path}/options.svg", "color": 0xFF989898}, 597 | "Button.Image::options:hovered": {"color": 0xFFC2C2C2}, 598 | "IconButton": {"margin": 0, "padding": 0, "background_color": 0x0}, 599 | "IconButton:hovered": {"background_color": 0x0}, 600 | "IconButton:checked": {"background_color": 0x0}, 601 | "IconButton:pressed": {"background_color": 0x0}, 602 | "IconButton.Image": {"color": 0xFFA8A8A8}, 603 | "IconButton.Image:hovered": {"color": 0xFFC2C2C2}, 604 | "IconButton.Image:pressed": {"color": 0xFFA4A4A4}, 605 | "IconButton.Image:checked": {"color": 0xFFFFFFFF}, 606 | "IconButton.Tooltip": {"color": 0xFF9E9E9E}, 607 | "IconButton.Image::OpenFolder": { 608 | "image_url": f"{icons_path}/open-folder.svg", 609 | "background_color": 0x0, 610 | "color": 0xFFA8A8A8, 611 | "tooltip": TOOLTIP_STYLE, 612 | }, 613 | "IconButton.Image::OpenConfig": { 614 | "tooltip": TOOLTIP_STYLE, 615 | "image_url": f"{icons_path}/open-config.svg", 616 | "background_color": 0x0, 617 | "color": 0xFFA8A8A8, 618 | }, 619 | "IconButton.Image::OpenLink": { 620 | "image_url": "resources/glyphs/link.svg", 621 | "background_color": 0x0, 622 | "color": 0xFFA8A8A8, 623 | "tooltip": TOOLTIP_STYLE, 624 | }, 625 | "IconButton.Image::OpenDocs": { 626 | "image_url": "resources/glyphs/docs.svg", 627 | "background_color": 0x0, 628 | "color": 0xFFA8A8A8, 629 | "tooltip": TOOLTIP_STYLE, 630 | }, 631 | "IconButton.Image::CopyToClipboard": { 632 | "image_url": "resources/glyphs/copy.svg", 633 | "background_color": 0x0, 634 | "color": 0xFFA8A8A8, 635 | }, 636 | "IconButton.Image::Export": { 637 | "image_url": f"{icons_path}/export.svg", 638 | "background_color": 0x0, 639 | "color": 0xFFA8A8A8, 640 | }, 641 | "IconButton.Image::Sync": { 642 | "image_url": "resources/glyphs/sync.svg", 643 | "background_color": 0x0, 644 | "color": 0xFFA8A8A8, 645 | }, 646 | "IconButton.Image::Upload": { 647 | "image_url": "resources/glyphs/upload.svg", 648 | "background_color": 0x0, 649 | "color": 0xFFA8A8A8, 650 | }, 651 | "IconButton.Image::FolderPicker": { 652 | "image_url": "resources/glyphs/folder.svg", 653 | "background_color": 0x0, 654 | "color": 0xFF929292, 655 | }, 656 | "ItemButton": {"padding": 2, "background_color": 0xFF444444, "border_radius": 4}, 657 | "ItemButton.Image::add": {"image_url": f"{icons_path}/plus.svg", "color": 0xFF06C66B}, 658 | "ItemButton.Image::remove": {"image_url": f"{icons_path}/trash.svg", "color": 0xFF1010C6}, 659 | "ItemButton:hovered": {"background_color": 0xFF333333}, 660 | "ItemButton:pressed": {"background_color": 0xFF222222}, 661 | "Tooltip": TOOLTIP_STYLE, 662 | } 663 | 664 | return style 665 | 666 | 667 | ## -------------------------------new options start-------------------------------- 668 | 669 | EXTENSION_FOLDER_PATH = pathlib.Path( 670 | omni.kit.app.get_app().get_extension_manager().get_extension_path_by_module(__name__) 671 | ) 672 | 673 | ## colors 674 | BUTTON_BG_COLOR = 0xFF24211F 675 | FRAME_BG_COLOR = 0xFF343433 676 | FRAME_HEAD_COLOR = 0xFF8F8F8F 677 | STRING_FIELD_LABEL_COLOR = 0xFF8F8F8F 678 | LABEL_COLOR = 0xFFD8D8D8 679 | LABEL_TITLE_COLOR = 0xFFCCCCCC 680 | DISABLED_LABEL_COLOR = 0xFF6E6E6E 681 | UNIT_COLOR = 0xFF6E6E6E 682 | LINE_COLOR = 0xFF8F8F8F 683 | TRIANGLE_COLOR = 0xFF8F8F8F 684 | TREEVIEW_ITEM_FONT = 14 685 | HEADER_FONT_SIZE = 16 686 | FONT_SIZE = 14 687 | 688 | 689 | def get_option_style(): 690 | style = { 691 | "CheckBox": {"border_radius": 2, "font_size": 12}, 692 | "CollapsableFrame": {"background_color": FRAME_BG_COLOR, "secondary_color": FRAME_BG_COLOR}, 693 | "CollapsableFrame:hovered": {"background_color": FRAME_BG_COLOR, "secondary_color": FRAME_BG_COLOR}, 694 | "Field::StringField": { 695 | "background_color": BUTTON_BG_COLOR, 696 | "color": STRING_FIELD_LABEL_COLOR, 697 | "font_size": FONT_SIZE, 698 | "font": f"{EXTENSION_FOLDER_PATH}/data/fonts/NVIDIASans_It.ttf", 699 | }, 700 | "Field::FloatField": { 701 | "color": LABEL_COLOR, 702 | "font_size": FONT_SIZE, 703 | "font": f"{EXTENSION_FOLDER_PATH}/data/fonts/NVIDIASans_Rg.ttf", 704 | }, 705 | "Field::FloatDrag": { 706 | "color": LABEL_COLOR, 707 | "font_size": FONT_SIZE, 708 | "alignment": ui.Alignment.LEFT, 709 | "font": f"{EXTENSION_FOLDER_PATH}/data/fonts/NVIDIASans_Rg.ttf", 710 | }, 711 | "Field::resetable": { 712 | "background_color": 0x0, 713 | "color": LABEL_COLOR, 714 | "font_size": 16, 715 | "padding": 4, 716 | "font": f"{EXTENSION_FOLDER_PATH}/data/fonts/NVIDIASans_Rg.ttf", 717 | }, 718 | "Line": {"color": LINE_COLOR}, 719 | "Label": { 720 | "color": LABEL_COLOR, 721 | "font_size": FONT_SIZE, 722 | "font": f"{EXTENSION_FOLDER_PATH}/data/fonts/NVIDIASans_Md.ttf", 723 | }, 724 | "Label::header": { 725 | "color": FRAME_HEAD_COLOR, 726 | "font_size": FONT_SIZE, 727 | "font": f"{EXTENSION_FOLDER_PATH}/data/fonts/NVIDIASans_Md.ttf", 728 | }, 729 | "Label::collapsable_header": { 730 | "color": FRAME_HEAD_COLOR, 731 | "font_size": HEADER_FONT_SIZE, 732 | "font": f"{EXTENSION_FOLDER_PATH}/data/fonts/NVIDIASans_Rg.ttf", 733 | }, 734 | "Label::index": { 735 | "color": LABEL_COLOR, 736 | "font_size": TREEVIEW_ITEM_FONT, 737 | "font": f"{EXTENSION_FOLDER_PATH}/data/fonts/NVIDIASans_Rg.ttf", 738 | }, 739 | "Label::density": { 740 | "color": UNIT_COLOR, 741 | "font_size": FONT_SIZE, 742 | "font": f"{EXTENSION_FOLDER_PATH}/data/fonts/NVIDIASans_Lt.ttf", 743 | }, 744 | "Label::exponent": { 745 | "color": UNIT_COLOR, 746 | "font_size": 8, 747 | "font": f"{EXTENSION_FOLDER_PATH}/data/fonts/NVIDIASans_Lt.ttf", 748 | }, 749 | "RadioButton": {"background_color": cl.transparent, "padding": 0}, 750 | "RadioButton:checked": {"background_color": cl.transparent, "padding": 0}, 751 | "RadioButton:hovered": {"background_color": cl.transparent, "padding": 0}, 752 | "RadioButton.Image": { 753 | "image_url": f"{EXTENSION_FOLDER_PATH}/icons/radio_off.svg", 754 | "color": LABEL_COLOR, 755 | }, 756 | "RadioButton.Image:hovered": { 757 | "image_url": f"{EXTENSION_FOLDER_PATH}/icons/radio_off.svg", 758 | "color": LABEL_COLOR, 759 | }, 760 | "RadioButton.Image:checked": {"image_url": f"{EXTENSION_FOLDER_PATH}/icons/radio_on.svg", "color": LABEL_COLOR}, 761 | "RadioButton:pressed": {"background_color": cl.transparent}, 762 | "Triangle": {"background_color": TRIANGLE_COLOR, "color": TRIANGLE_COLOR}, 763 | "Rectangle::reset_invalid": {"background_color": 0xFF505050, "border_radius": 1}, 764 | "Rectangle::reset": {"background_color": 0xFFA07D4F, "border_radius": 1}, 765 | "ScrollingFrame": {"background_color": FRAME_BG_COLOR}, 766 | } 767 | return style 768 | -------------------------------------------------------------------------------- /lightwheel/MJCF2USD/connection/mjcf2usd_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import copy 3 | import shutil 4 | import numpy as np 5 | import xml.etree.ElementTree as ET 6 | from collections import defaultdict 7 | 8 | from scipy.spatial.transform import Rotation as R 9 | 10 | import omni.kit.commands 11 | import omni.physx 12 | 13 | from pxr import Sdf, Gf, UsdPhysics, PhysxSchema, Usd, UsdShade, UsdGeom 14 | 15 | class XMLHandler: 16 | def __init__(self, xml_path): 17 | """ 18 | Initialize the XML processor. 19 | 20 | Args: 21 | xml_path (str): Path to the XML file. 22 | """ 23 | self.xml_path = xml_path 24 | self.tree = ET.parse(xml_path) 25 | self.root = self.tree.getroot() 26 | self.unnamed_body_cnt = 0 27 | self.replicate_name_list = [] 28 | 29 | def __enter__(self): 30 | return self 31 | 32 | def __exit__(self, exc_type, exc_val, exc_tb): 33 | self.tree = None 34 | self.root = None 35 | return False 36 | 37 | def save_xml(self): 38 | """ 39 | Write the modified XML tree back to the original file path. 40 | 41 | Returns: 42 | bool: True if the write operation succeeded, False otherwise. 43 | """ 44 | try: 45 | self.tree.write(self.xml_path) 46 | print("Original XML file has been updated.") 47 | return True 48 | except Exception as e: 49 | print(f"Failed to write XML file: {e}") 50 | return False 51 | 52 | def fix_site_placement(self): 53 | """ 54 | Fix the placement of elements in the XML file by ensuring they are located 55 | inside the element if it exists under the same parent body. 56 | 57 | Returns: 58 | bool: True if any changes were made, False otherwise. 59 | """ 60 | # Iterate over all elements in the XML 61 | modified = False 62 | for parent_body in self.root.findall(".//body"): 63 | object_body = None 64 | sites_to_move = [] 65 | 66 | # Search for a child with name="object" 67 | for child in parent_body: 68 | if child.tag == "body" and child.get("name") == "object": 69 | object_body = child 70 | 71 | # If found, collect all elements directly under parent_body 72 | if object_body is not None: 73 | for child in list(parent_body): 74 | if child.tag == "site": 75 | sites_to_move.append(child) 76 | 77 | # Move collected elements into the "object" body 78 | if sites_to_move: 79 | for site in sites_to_move: 80 | parent_body.remove(site) 81 | object_body.append(site) 82 | modified = True 83 | 84 | if modified: 85 | print(f" Fixed misplaced elements in {self.xml_path}") 86 | 87 | return modified 88 | 89 | def get_materials(self): 90 | """Get material information from the XML file""" 91 | # Create dict for materials and textures 92 | try : 93 | textures = {} 94 | materials = {} 95 | 96 | asset = self.root.find('asset') 97 | 98 | parent_dir = os.path.dirname(self.xml_path) 99 | for texture in asset.findall('texture'): 100 | textures[texture.get('name')] = { 101 | 'file': os.path.join(parent_dir, texture.get('file')), 102 | 'type': texture.get('type') 103 | } 104 | 105 | for material in asset.findall('material'): 106 | name = material.get('name') 107 | texture_name = material.get('texture', '') 108 | if texture_name: 109 | texture_file = textures.get(texture_name).get('file') 110 | texture_type = textures.get(texture_name).get('type') 111 | else: 112 | texture_file = '' 113 | texture_type = '' 114 | 115 | material_data = { 116 | 'rgba': material.get('rgba'), 117 | 'shininess': material.get('shininess'), 118 | 'specular': material.get('specular'), 119 | 'texture_file': texture_file, 120 | 'texture_type': texture_type 121 | } 122 | material_name = 'material_' + name 123 | material_name = convert_name_from_mjcf_2_usd(material_name) 124 | materials[material_name] = material_data 125 | 126 | return materials 127 | except Exception as e: 128 | print(f"Skip get materials from XML: {e}") 129 | return {} 130 | 131 | def get_geom_material_map(self): 132 | "Get the mapping between geometries and their associated materials." 133 | geom_material_dict = {} 134 | geom_elements = [elem for elem in self.root.iter() if elem.tag == 'geom'] 135 | for geom in geom_elements: 136 | mesh = geom.get('mesh') 137 | material = geom.get('material') 138 | if mesh and material: 139 | material_name = 'material_'+ material 140 | material_name = convert_name_from_mjcf_2_usd(material_name) 141 | geom_material_dict[mesh] = material_name 142 | return geom_material_dict 143 | 144 | def get_joints(self): 145 | 146 | # Recursive function to traverse all 'body' and 'joint' nodes 147 | def find_joints_in_body(body, parent_name=""): 148 | # Get all 'joint' nodes under the current 'body' 149 | body_name = body.get("name", "") 150 | full_parent_name = f"{parent_name}_{body_name}" if parent_name else body_name 151 | 152 | for joint in body.findall("joint"): 153 | name = joint.get("name", "") 154 | if not name: 155 | continue 156 | 157 | # Extract joint attributes 158 | joint_data = { 159 | 'damping': joint.get("damping", ""), 160 | 'stiffness': joint.get("stiffness", ""), 161 | 'frictionloss': joint.get("frictionloss", ""), 162 | # 'type': joint.get("type", "hinge"), 163 | # 'axis': joint.get("axis", "0 0 1"), 164 | # 'pos': joint.get("pos", "0 0 0"), 165 | # 'range': joint.get("range", ""), 166 | # 'armature': joint.get("armature", ""), 167 | # 'limited': joint.get("limited", ""), 168 | # 'parent_body': full_parent_name, 169 | } 170 | 171 | joints_info[name] = joint_data 172 | 173 | for child_body in body.findall("body"): 174 | find_joints_in_body(child_body, full_parent_name) 175 | 176 | """Get joint information from the XML file""" 177 | joints_info = {} 178 | 179 | worldbody = self.root.find(".//worldbody") 180 | if worldbody is None: 181 | return joints_info 182 | 183 | for body in worldbody.findall("body"): 184 | find_joints_in_body(body) 185 | 186 | return joints_info 187 | 188 | def get_density(self): 189 | class_density_dict = {} 190 | geom_density_dict = {} 191 | 192 | # get class_densitys_dict 193 | defaultbody = self.root.find(".//default") 194 | if defaultbody is None: 195 | return geom_density_dict 196 | 197 | for default in defaultbody.findall("default"): 198 | class_name = default.get("class") 199 | if not class_name: 200 | continue 201 | 202 | geom = default.find("geom") 203 | if geom is None: 204 | continue 205 | 206 | density = geom.get("density") 207 | if not density: 208 | continue 209 | 210 | class_density_dict[class_name] = density 211 | 212 | 213 | #get geom_densitys_dict 214 | worldbody = self.root.find(".//worldbody") 215 | for body in worldbody.iter('body'): 216 | density = self.get_body_density(body,class_density_dict) 217 | body_name = body.attrib.get("name") 218 | 219 | if body_name is not None and density is not None: 220 | geom_density_dict[body_name] = density 221 | 222 | return geom_density_dict 223 | 224 | def get_body_density(self,body,class_density_dict): 225 | """ 226 | The density of all collision geoms under a body is the same, 227 | and it is equal to the body's density. 228 | """ 229 | for geom in body.findall("geom"): 230 | class_name = geom.attrib.get("class") 231 | if class_name and "col" in class_name: 232 | density = class_density_dict.get(class_name) 233 | if density is not None: 234 | return density 235 | return None 236 | 237 | def preprocess_refquat_in_meshes(self): 238 | """ 239 | Preprocess and correct the 'refquat' attribute from MJCF mesh definitions into corresponding geom elements. 240 | 241 | In MJCF files, if a element under has a 'refquat' attribute, the current importer does not 242 | handle it properly. This function collects all mesh names with a 'refquat' attribute and their values, 243 | removes the attribute from the original XML, and stores them in a dictionary. 244 | 245 | Later, these 'refquat' values are applied to corresponding elements to correct their orientation. 246 | """ 247 | try : 248 | mesh_refquat_dict = defaultdict(list) 249 | asset_elem = self.root.find(".//asset") 250 | for mesh in asset_elem.findall("mesh"): 251 | mesh_name = mesh.get("name") 252 | mesh_refquat = mesh.get("refquat") 253 | if mesh_name is not None and mesh_refquat is not None: 254 | mesh_name = convert_name_from_mjcf_2_usd(mesh_name) 255 | del mesh.attrib['refquat'] 256 | 257 | mesh_refquat = list(map(float, mesh_refquat.strip().split())) 258 | mesh_refquat_dict[mesh_name] = mesh_refquat 259 | 260 | self.geom_add_refquat(self.root,mesh_refquat_dict) 261 | except Exception as e: 262 | print(f"Skip processing refquat in meshes: {e}") 263 | return False 264 | 265 | def geom_add_refquat(self,parent,mesh_refquat_dict): 266 | for child in list(parent): 267 | mesh_name = child.get("mesh") 268 | if child.tag == 'geom' and mesh_name is not None: 269 | mesh_name = convert_name_from_mjcf_2_usd(mesh_name) 270 | if mesh_name in mesh_refquat_dict: 271 | mesh_ref_quat = mesh_refquat_dict.get(mesh_name) 272 | mesh_quat = self.get_quat(child) 273 | self.elem_update_with_ref_quat(child,mesh_quat,mesh_ref_quat) 274 | 275 | self.geom_add_refquat(child,mesh_refquat_dict) 276 | 277 | def get_quat(self, elem, default_eulerseq="xyz"): 278 | #TODO support xyaxes,zaxis 279 | """ 280 | Get rotation representation from XML element and convert to quaternion [w, x, y, z] 281 | 282 | Parameters: 283 | elem: XML element 284 | default_eulerseq: Default Euler angle sequence, defaults to "xyz" (x-y-z) 285 | 286 | Returns: 287 | Quaternion [w, x, y, z] 288 | 289 | Exceptions: 290 | ValueError: Raised when encountering invalid input 291 | 292 | Supported attributes (by priority): 293 | 1. quat: [w, x, y, z] 294 | 2. axisangle: [x, y, z, angle] 295 | 3. euler: [e1, e2, e3] + eulerseq 296 | """ 297 | # Handle angle conversion factor 298 | use_degree = self.is_angle_in_degrees() 299 | angle_factor = np.pi / 180.0 if use_degree else 1.0 300 | 301 | # 1. Handle quaternion representation 302 | quat_str = elem.get("quat") 303 | if quat_str: 304 | try: 305 | quat = list(map(float, quat_str.split())) 306 | if len(quat) != 4: 307 | raise ValueError(f"quat requires 4 values, got {len(quat)}: {quat_str}") 308 | return quat 309 | except Exception as e: 310 | raise ValueError(f"Invalid quat value: {quat_str}") from e 311 | 312 | # 2. Handle axis-angle representation (x, y, z, angle) 313 | axisangle_str = elem.get("axisangle") 314 | if axisangle_str: 315 | try: 316 | coords = list(map(float, axisangle_str.split())) 317 | if len(coords) != 4: 318 | raise ValueError(f"axisangle requires 4 values, got {len(coords)}: {axisangle_str}") 319 | 320 | x, y, z, angle_val = coords[0], coords[1], coords[2], coords[3] 321 | angle_rad = angle_val * angle_factor 322 | 323 | # Normalize axis vector 324 | axis = np.array([x, y, z]) 325 | axis_norm = np.linalg.norm(axis) 326 | if axis_norm < 1e-8: 327 | raise ValueError(f"axisangle rotation axis is zero vector: {axisangle_str}") 328 | 329 | axis /= axis_norm 330 | 331 | # Create rotation 332 | rot = R.from_rotvec(axis * angle_rad) 333 | 334 | # Convert to MuJoCo format quaternion [w, x, y, z] 335 | scipy_quat = rot.as_quat() 336 | return [scipy_quat[3], scipy_quat[0], scipy_quat[1], scipy_quat[2]] 337 | 338 | except Exception as e: 339 | raise ValueError(f"Invalid axisangle value: {axisangle_str}") from e 340 | 341 | # 3. Handle euler representation (e1, e2, e3) 342 | euler_str = elem.get("euler") 343 | if euler_str: 344 | try: 345 | coords = list(map(float, euler_str.split())) 346 | if len(coords) != 3: 347 | raise ValueError(f"euler requires 3 values, got {len(coords)}: {euler_str}") 348 | 349 | euler_values = coords[:3] 350 | eulerseq = elem.get("eulerseq", default_eulerseq) 351 | 352 | # Create rotation 353 | rot = R.from_euler(eulerseq, euler_values, degrees=use_degree) 354 | 355 | # Convert to MuJoCo format quaternion [w, x, y, z] 356 | scipy_quat = rot.as_quat() 357 | return [scipy_quat[3], scipy_quat[0], scipy_quat[1], scipy_quat[2]] 358 | 359 | except Exception as e: 360 | raise ValueError(f"Invalid euler value: {euler_str}") from e 361 | 362 | return [1, 0, 0, 0] 363 | 364 | def is_angle_in_degrees(self): 365 | """ 366 | Check whether the angle unit in the XML is set to degrees. 367 | 368 | This function examines the tag's 'angle' attribute: 369 | - Returns True if angle="degree" 370 | - Returns False if angle="radian" 371 | - Returns True by default if the attribute is missing (degrees are assumed) 372 | 373 | 374 | Returns: 375 | bool: True if using degrees, False if using radians 376 | """ 377 | compiler = self.root.find('compiler') 378 | if compiler is not None: 379 | angle_unit = compiler.get('angle', 'degree') 380 | return angle_unit.lower() == 'degree' 381 | return True 382 | 383 | def add_replicated_item_to_grandparent(self, parent, item, father_pos, father_quat, replicate_depth): 384 | """ 385 | Insert a replicated XML element under its grandparent node, transforming its position and orientation based on the parent's pose. 386 | 387 | Args: 388 | parent (Element): The parent XML element of the item to be replicated. 389 | item (Element): The XML element to be inserted (replicated item). 390 | father_pos (list[float]): The position of the parent as a list of 3 floats [x, y, z]. 391 | father_quat (scipy.spatial.transform.Rotation): The orientation of the parent as a scipy Rotation object (quaternion). 392 | replicate_depth (int): The current depth of replication, used to track top-level replication. 393 | 394 | Returns: 395 | Element or None: The newly inserted element under the grandparent, or None if insertion failed. 396 | """ 397 | if item is None or parent is None: 398 | return 399 | 400 | pos = [float(x) for x in item.attrib.get('pos', '0 0 0').split()] 401 | quat = [float(x) for x in item.attrib.get('quat', '1 0 0 0').split()] 402 | 403 | new_quat = [quat[1], quat[2], quat[3], quat[0]] 404 | new_quat = R.from_quat(new_quat) 405 | 406 | new_quat = father_quat * new_quat 407 | 408 | pos_diff = np.array([pos[i] for i in range(3)]) 409 | 410 | rotation = father_quat 411 | rotated_pos_local = rotation.apply(pos_diff) 412 | now_pos = [father_pos[i] + rotated_pos_local[i] for i in range(3)] 413 | 414 | def find_grandparent(elem, target): 415 | for child in elem: 416 | if child is target: 417 | return elem 418 | result = find_grandparent(child, target) 419 | if result is not None: 420 | return result 421 | return None 422 | 423 | grandparent = find_grandparent(self.root, parent) 424 | if grandparent is None: 425 | return 426 | 427 | item.set('pos', ' '.join(map(str, now_pos))) 428 | 429 | new_quat = R.as_quat(new_quat) 430 | new_quat = [new_quat[3], new_quat[0], new_quat[1], new_quat[2]] 431 | item.set('quat', ' '.join(map(str, new_quat))) 432 | geom_name = item.get('name', None) 433 | if geom_name is not None and replicate_depth == 1: 434 | self.replicate_name_list.append(item) 435 | 436 | grandparent.append(item) 437 | return 438 | 439 | def traverse_and_expand_replicates(self, element, father_element, replicate_depth = 0): 440 | """ 441 | Recursively traverse the XML tree, expanding tags by duplicating their child elements 442 | according to the specified count, position offset, and rotation. The replicated elements are inserted 443 | into their grandparent node with updated position and orientation. The original tag and its 444 | children are removed from the tree after expansion. 445 | 446 | Args: 447 | element (Element): The current XML element being traversed. 448 | father_element (Element): The parent XML element of the current element. 449 | replicate_depth (int, optional): The current depth of nested tags. Defaults to 0. 450 | """ 451 | if element is None: 452 | return 453 | 454 | for item in list(element): 455 | self.traverse_and_expand_replicates(item, element, replicate_depth + (1 if element.tag == 'replicate' else 0)) 456 | 457 | if element.tag == "replicate": 458 | count = int(element.attrib.get('count', 1)) 459 | offset = [float(x) for x in element.attrib.get('offset', '0 0 0').split()] 460 | euler = [float(x) for x in element.attrib.get('euler', '0 0 0').split()] 461 | is_degree = self.is_angle_in_degrees() 462 | rot = R.from_euler('xyz', euler, degrees=is_degree) 463 | new_quat = R.from_euler('xyz', [0, 0, 0], degrees=is_degree) 464 | 465 | for item in list(element): 466 | if item.tag == 'replicate': 467 | continue 468 | for i in range(count): 469 | new_geom = copy.deepcopy(item) 470 | 471 | new_pos = [ 472 | offset[0] * i, 473 | offset[1] * i, 474 | offset[2] * i 475 | ] 476 | 477 | if i > 0: 478 | new_quat = rot * new_quat 479 | 480 | self.add_replicated_item_to_grandparent(element, new_geom, new_pos, new_quat, replicate_depth + 1) 481 | if item in element: 482 | element.remove(item) 483 | 484 | father_element.remove(element) 485 | return 486 | 487 | def expand_replicates_fields(self): 488 | """Recursively expand and remove tags.""" 489 | 490 | worldbody = self.root.find(".//worldbody") 491 | if worldbody is not None: 492 | for element in worldbody: 493 | self.traverse_and_expand_replicates(element, worldbody) 494 | 495 | dict_name = {} 496 | for geom in self.replicate_name_list: 497 | geom_name = geom.get('name') 498 | if geom_name not in dict_name: 499 | dict_name[geom_name] = 0 500 | 501 | geom.set('name', geom_name + '_' + str(dict_name[geom_name] )) 502 | dict_name[geom_name] += 1 503 | 504 | def elem_update_with_ref_quat(self,elem,mesh_quat,mesh_ref_quat): 505 | if mesh_ref_quat and mesh_quat: 506 | 507 | mesh_ref_quat_R = convert_quat_mjcf_2_scipy(mesh_ref_quat) 508 | mesh_quat_R = convert_quat_mjcf_2_scipy(mesh_quat) 509 | 510 | r1 = R.from_quat(mesh_ref_quat_R) 511 | r2 = R.from_quat(mesh_quat_R) 512 | r1_inv = r1.inv() 513 | 514 | q_list = (r2 * r1_inv).as_quat().tolist() 515 | quat_mjcf = convert_quat_scipy_2_mjcf(q_list) 516 | elem.set("quat", ' '.join(map(str, quat_mjcf))) 517 | 518 | elif mesh_ref_quat and not mesh_quat: 519 | 520 | mesh_ref_quat_R = convert_quat_mjcf_2_scipy(mesh_ref_quat) 521 | 522 | r = R.from_quat(mesh_ref_quat_R) 523 | r_inv = r.inv() 524 | 525 | q_list = r_inv.as_quat().tolist() 526 | quat_mjcf = convert_quat_scipy_2_mjcf(q_list) 527 | elem.set("quat", ' '.join(map(str, quat_mjcf))) 528 | 529 | def change_unnamed_geom(self, root, father_name, unnamed_geom : dict): 530 | """ 531 | Change unnamed geometries in the XML tree by assigning them unique names and storing their position, orientation, and size. 532 | Args: 533 | root (Element): The father element to start processing. 534 | father_name (str): The name of the father body, used for naming unnamed geometries. 535 | unnamed_geom (dict): A dictionary to store unnamed geometries with their attributes. 536 | """ 537 | cnt = 0 538 | for item in root: 539 | if item.tag == 'body': 540 | if item.get('name') is None: 541 | self.unnamed_body_cnt += 1 542 | body_name = 'unnamed_body_' + str(self.unnamed_body_cnt) 543 | else: 544 | body_name = item.get('name') 545 | item.set('name', body_name) 546 | if item.tag == 'geom' and item.get('mesh') is None and item.get('name') is None: 547 | name = item.get('name') 548 | if name is None: 549 | cnt += 1 550 | # if type is not set, default is sphere in mujoco 551 | name = (father_name if root.tag != 'worldbody' else 'worldbody') + '_' + item.get('type', 'sphere') + '_' + str(cnt) 552 | item.set('name', name) 553 | 554 | pos = None if item.get('pos') is None else [float(i) for i in (item.get('pos').split())] 555 | size = None if item.get('size') is None else [float(i) for i in (item.get('size').split())] 556 | if len(size) == 1 or item.get('type') is None: 557 | size = [size[0], size[0], size[0]] 558 | elif item.get('type') == 'capsule' or item.get('type') == 'cylinder': 559 | # For capsule and cylinder, size is [radius, length], so the first two values are the same 560 | size = [size[0], size[0], size[1]] 561 | unnamed_geom[name] = [pos, self.get_quat(item), size] 562 | self.change_unnamed_geom(item, body_name if item.tag == 'body' else None, unnamed_geom) 563 | 564 | def debug(stage): 565 | root_prim = stage.GetPrimAtPath('/root') 566 | 567 | def mjcf_to_usd(mjcf_path, usd_path='',need_save_tmp_xml=False): 568 | if usd_path == '': 569 | usd_path = mjcf_path[:-4] + ".usd" 570 | 571 | tmp_mjcf_path = mjcf_path[:-4] + "_tmp.xml" 572 | shutil.copy2(mjcf_path, tmp_mjcf_path) 573 | 574 | with XMLHandler(tmp_mjcf_path) as xml_handler: 575 | """preprocess mjcf xml""" 576 | xml_handler.preprocess_refquat_in_meshes() 577 | xml_handler.expand_replicates_fields() 578 | xml_handler.fix_site_placement() 579 | unmeshed_geom = {} 580 | xml_handler.change_unnamed_geom(xml_handler.root.find(".//worldbody"), None, unmeshed_geom) 581 | xml_handler.save_xml() 582 | 583 | """Create a new empty USD stage""" 584 | context = omni.usd.get_context() 585 | context.new_stage() 586 | stage = context.get_stage() 587 | 588 | "Setting up import configuration" 589 | status, import_config = omni.kit.commands.execute("MJCFCreateImportConfig") 590 | import_config.set_fix_base(False) 591 | import_config.set_make_default_prim(False) 592 | omni.kit.commands.execute( 593 | "MJCFCreateAsset", 594 | mjcf_path=xml_handler.xml_path, 595 | import_config=import_config, 596 | prim_path="/root" 597 | ) 598 | 599 | "Fix joints" 600 | joints_info = xml_handler.get_joints() 601 | fix_joints(stage, joints_info) 602 | 603 | "Resolve mesh name conflicts caused by native import" 604 | fix_repeat_mesh_name() 605 | 606 | fix_unmeshed_geom_info(stage.GetPrimAtPath('/meshes'), unmeshed_geom) 607 | 608 | fix_reference_missing_transform() 609 | 610 | "Copy texture assets to the output directory." 611 | materials = xml_handler.get_materials() 612 | transfer_texture(usd_path, materials) 613 | 614 | "Recreate materials" 615 | clear_default_error_material(stage) 616 | geom_meterial_map = xml_handler.get_geom_material_map() 617 | create_material_to_mesh(stage, materials, geom_meterial_map) 618 | 619 | "Fix density" 620 | density_dict = xml_handler.get_density() 621 | fix_physics(stage, density_dict) 622 | 623 | apply_trimesh_collision(stage) 624 | 625 | # # converted by LW rule(Get directory as model name) 626 | parent_dir = os.path.dirname(mjcf_path) 627 | 628 | # save stage 629 | save_result = context.save_as_stage(usd_path) 630 | if not save_result: 631 | context.close_stage() 632 | return 633 | context.close_stage() 634 | 635 | clear_temp_usd(parent_dir) 636 | if not need_save_tmp_xml: 637 | os.remove(tmp_mjcf_path) 638 | return 639 | 640 | def fix_unmeshed_geom_info(mesh_prim, unmeshed_geom: dict): 641 | """ 642 | Adjust the size of geometries in the USD stage that are not associated with meshes. 643 | 644 | Args: 645 | mesh_prim (Usd.Prim): The USD stage node containing the geometries. 646 | unnamed_geom (dict): A dictionary mapping geometry names to their size information. 647 | """ 648 | op_types = [ 649 | "xformOp:translate", 650 | "xformOp:orient", 651 | "xformOp:scale" 652 | ] 653 | for prim in mesh_prim.GetChildren(): 654 | if prim.GetTypeName() == "Xform": 655 | if prim.GetName() in unmeshed_geom: 656 | transforms = unmeshed_geom[prim.GetName()] 657 | 658 | # process size attribute 659 | xformable = UsdGeom.Xformable(prim) 660 | ops = xformable.GetOrderedXformOps() 661 | has_op_types = [op.GetOpName() in op_types for op in ops] 662 | for i, op_type in enumerate(op_types): 663 | if transforms[i] is not None: 664 | if op_type not in has_op_types: 665 | if op_type == "xformOp:translate": 666 | pos_op = xformable.AddTranslateOp() 667 | pos = transforms[i] 668 | pos_op.Set(Gf.Vec3d(pos[0], pos[1], pos[2])) 669 | elif op_type == "xformOp:orient": 670 | quat = transforms[i] 671 | rot_op = xformable.AddOrientOp() 672 | rot_op.Set(Gf.Quatf(quat[3], quat[0], quat[1], quat[2])) 673 | elif op_type == "xformOp:scale": 674 | scale_op = xformable.AddScaleOp() 675 | scale = transforms[i] 676 | scale_op.Set(Gf.Vec3d(scale[0], scale[1], scale[2])) 677 | fix_unmeshed_geom_info(prim, unmeshed_geom) 678 | 679 | def get_all_sites(prim, sites=[]): 680 | """Recursively get all sites under the given prim""" 681 | for child in prim.GetChildren(): 682 | if "sites" in child.GetName(): 683 | for site in child.GetChildren(): 684 | sites.append(site) 685 | get_all_sites(child, sites) 686 | 687 | def fix_physics(stage,density_dict): 688 | # add physics scence 689 | scene = PhysxSchema.PhysxSceneAPI.Get(stage, "/physicsScene") 690 | scene.CreateBroadphaseTypeAttr().Set("MBP") 691 | scene.CreateSolverTypeAttr().Set("TGS") 692 | 693 | for prim in stage.Traverse(): 694 | if prim.HasAPI(UsdPhysics.RigidBodyAPI) and str(prim.GetName()) in density_dict: 695 | density = float(density_dict.get(prim.GetName())) 696 | mass_api = UsdPhysics.MassAPI.Apply(prim) 697 | mass_api.CreateDensityAttr().Set(density) 698 | 699 | def apply_trimesh_collision(stage): 700 | for prim in stage.Traverse(): 701 | if prim.HasAPI(UsdPhysics.MeshCollisionAPI): 702 | mesh_collision_api = UsdPhysics.MeshCollisionAPI.Apply(prim) 703 | mesh_collision_api.GetApproximationAttr().Set("none") 704 | 705 | 706 | def clear_default_error_material(stage): 707 | """ clear materials under Looks scope """ 708 | default_prim = stage.GetDefaultPrim() 709 | if not default_prim: 710 | return 711 | 712 | looks_path = default_prim.GetPath().AppendChild("Looks") 713 | looks_prim = stage.GetPrimAtPath(looks_path) 714 | for material in looks_prim.GetChildren(): 715 | stage.RemovePrim(material.GetPath()) 716 | 717 | def create_material_to_mesh(stage,materials,geom_meterial_map): 718 | 719 | default_prim = stage.GetDefaultPrim() 720 | if not default_prim: 721 | return 722 | 723 | #creat material 724 | material_path_map = {} 725 | looks_path = default_prim.GetPath().AppendChild("Looks") 726 | for key in materials: 727 | material_name = str(key) 728 | material_path = looks_path.AppendChild(material_name) 729 | 730 | result = omni.kit.commands.execute('CreateAndBindMdlMaterialFromLibrary', 731 | mdl_name='OmniPBR.mdl', 732 | mtl_name='OmniPBR', 733 | mtl_created_list=[material_path], 734 | bind_selected_prims=['/model/Looks'], 735 | prim_name=material_name) 736 | if result[0]: 737 | material_path_map.setdefault(material_name,material_path) 738 | 739 | 740 | for new_material_path in material_path_map.values(): 741 | material_prim = stage.GetPrimAtPath(new_material_path) 742 | if not material_prim.IsA(UsdShade.Material): 743 | continue 744 | 745 | preview_surface = UsdShade.Shader(material_prim.GetPrimAtPath( 746 | material_prim.GetPath().AppendChild("Shader"))) 747 | 748 | if not preview_surface: 749 | continue 750 | 751 | material_name = material_prim.GetName() 752 | if material_name not in materials: 753 | continue 754 | material_data = materials[material_name] 755 | 756 | # Set rgba 757 | if material_data.get('rgba'): 758 | rgba = [float(x) for x in material_data['rgba'].split(' ')] 759 | if len(rgba) > 3: 760 | preview_surface.CreateInput('diffuse_color_constant', Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(rgba[0], rgba[1], rgba[2])) 761 | # if len(rgba) == 4: 762 | # preview_surface.CreateInput('opacity_constant', Sdf.ValueTypeNames.Float).Set(rgba[3]) 763 | 764 | # Set shininess) 765 | # if material_data.get('shininess'): 766 | # shininess = float(material_data['shininess']) 767 | # preview_surface.CreateInput('reflection_roughness_constant', Sdf.ValueTypeNames.Float).Set(1.0 - shininess) 768 | 769 | # Set specular) 770 | if material_data.get('specular'): 771 | specular = float(material_data['specular']) 772 | preview_surface.CreateInput('specular_level', Sdf.ValueTypeNames.Float).Set(specular) 773 | 774 | # Set texture_file 775 | if material_data.get('texture_file'): 776 | preview_surface.CreateInput('diffuse_texture', Sdf.ValueTypeNames.Asset).Set(material_data['texture_file']) 777 | 778 | apply_material_to_geom(material_path_map,geom_meterial_map) 779 | 780 | def apply_material_to_geom(material_path_map,geom_meterial_map): 781 | """Apply materials to geometry prims in the USD stage.""" 782 | stage = omni.usd.get_context().get_stage() 783 | 784 | visuals_prim = stage.GetPrimAtPath("/visuals") 785 | for references_prim in visuals_prim.GetChildren(): 786 | 787 | for ref in references_prim.GetChildren(): 788 | ref_name = ref.GetPath() 789 | geom_name = str(ref_name).split("/")[-1] 790 | material = geom_meterial_map.get(geom_name) 791 | material_path = material_path_map.get(material) 792 | 793 | if material_path: 794 | omni.kit.commands.execute('BindMaterial', 795 | material_path=material_path, 796 | prim_path=[ref_name], 797 | strength=['weakerThanDescendants'], 798 | material_purpose='') 799 | 800 | def fix_repeat_mesh_name(): 801 | # Fix the issue of duplicate 'mesh' names created by omni.MJCFCreateAsset 802 | stage = omni.usd.get_context().get_stage() 803 | meshes_prim = stage.GetPrimAtPath("/meshes") 804 | if meshes_prim is not None: 805 | xformes = meshes_prim.GetChildren() 806 | for xform in xformes: 807 | meshes = xform.GetChildren() 808 | for mesh in meshes: 809 | if mesh is not None: 810 | name = mesh.GetName() 811 | if str(name) == "mesh": 812 | oldPrimPath = mesh.GetPath() 813 | tmpPath = str(oldPrimPath).rsplit('/',1)[0] 814 | newPrimPath = tmpPath +"/"+xform.GetName() 815 | 816 | omni.kit.commands.execute('MovePrim', 817 | path_from=Sdf.Path(oldPrimPath), 818 | path_to=Sdf.Path(newPrimPath), 819 | destructive=False, 820 | stage_or_context=omni.usd.get_context().get_stage()) 821 | 822 | def get_xfom_need_process(root_prim, set1, set2): 823 | """ 824 | Traverse the USD stage to categorize referenced paths into two sets: `set1` and `set2`. 825 | 826 | - `set1`: Stores paths of prims that are referenced by a single parent prim, 827 | and the parent prim does not reference any other prims. 828 | Example: If prim A references prim B, and A does not reference any other prims, 829 | then B's path is added to `set1`. 830 | 831 | - `set2`: Stores paths of prims that are referenced by a parent prim which also references 832 | other prims simultaneously. 833 | Example: If prim D references both prims B and C, then B's and C's paths are added to `set2`. 834 | 835 | Args: 836 | root_prim (Usd.Prim): The root prim to start traversal. 837 | set1 (set): A set to store paths of prims meeting the `set1` criteria. 838 | set2 (set): A set to store paths of prims meeting the `set2` criteria. 839 | """ 840 | for prim in root_prim.GetChildren(): 841 | if prim.GetTypeName() == 'Xform' and prim.GetName() in ['collisions', 'visuals']: 842 | cnt = 0 843 | for ref in Usd.PrimCompositionQuery.GetDirectReferences(prim).GetCompositionArcs(): 844 | target_path = str(ref.GetTargetPrimPath()) 845 | if '/meshes/' in target_path: 846 | cnt += 1 847 | 848 | if cnt > 1: 849 | for ref in Usd.PrimCompositionQuery.GetDirectReferences(prim).GetCompositionArcs(): 850 | target_path = str(ref.GetTargetPrimPath()) 851 | set2.add(target_path) 852 | elif cnt == 1: 853 | set1.add(target_path) 854 | 855 | get_xfom_need_process(prim, set1, set2) 856 | 857 | def fix_reference_missing_transform(): 858 | """ 859 | Transforms on intermediate references are ignored when using multiple levels of references. 860 | When an Xform prim references multiple Xform (e.g., B and C) simultaneously at the same composition site, 861 | B and C's transform must be pushed down to their child. This is unnecessary for single Xform references 862 | """ 863 | try: 864 | stage = omni.usd.get_context().get_stage() 865 | set1, set2 = set(), set() 866 | get_xfom_need_process(stage.GetPrimAtPath("/root"), set1, set2) 867 | inter_set = set2.intersection(set1) 868 | meshes_prim = stage.GetPrimAtPath("/meshes") 869 | if meshes_prim is not None: 870 | xformes = meshes_prim.GetChildren() 871 | for xform in xformes: 872 | xform_path = str(xform.GetPath()) 873 | if xform_path in set2: 874 | meshes = xform.GetChildren() 875 | for mesh in meshes: 876 | copy_xform_op(xform, mesh, xform_path in inter_set) 877 | except Exception as e: 878 | print(f"Skipped fixing reference missing transform due to error: {e}") 879 | 880 | def transfer_texture(usd_path,materials): 881 | destination_dir = os.path.dirname(usd_path) 882 | destination_dir = os.path.join(destination_dir,"texture","") 883 | 884 | if not os.path.exists(destination_dir): 885 | os.makedirs(destination_dir,exist_ok=True) 886 | 887 | for material_data in materials.values(): 888 | texture_file = str(material_data['texture_file']) 889 | if texture_file: 890 | result = shutil.copy(texture_file,destination_dir) 891 | if os.path.exists(result): 892 | material_data['texture_file'] = result 893 | 894 | def fix_joints(stage, joints_info): 895 | 896 | def get_all_joints(prim, joints=[]): 897 | """Recursively get all joints under the given prim""" 898 | for child in prim.GetChildren(): 899 | if "Joint" in child.GetTypeName(): 900 | joints.append(child) 901 | get_all_joints(child, joints) 902 | 903 | default_prim = stage.GetDefaultPrim() 904 | joints_prim = default_prim.GetChild('joints') 905 | all_joints = [] 906 | get_all_joints(joints_prim, all_joints) 907 | 908 | for joint in all_joints: 909 | joint_data = joints_info.get(joint.GetName()) 910 | if joint_data: 911 | set_joint_properties(joint,joint_data) 912 | 913 | def if_joints_linked(stage,joint_path,target_path)->bool: 914 | """ 915 | Check whether a joint is connected to a specific target prim. 916 | 917 | Args: 918 | stage: The USD stage. 919 | joint_path: The Sdf.Path to the joint prim. 920 | target_path: The Sdf.Path to the target prim (e.g., a rigid body). 921 | 922 | Returns: 923 | True if the target prim is linked to the joint via body0 or body1; 924 | False otherwise. 925 | """ 926 | joint = UsdPhysics.Joint.Get(stage,joint_path) 927 | Rel_path_0 = joint.GetBody0Rel().GetTargets() 928 | Rel_path_1 = joint.GetBody1Rel().GetTargets() 929 | 930 | if target_path in Rel_path_0 or target_path in Rel_path_1: 931 | return True 932 | else: 933 | return False 934 | 935 | def set_joint_properties(joint,joint_data): 936 | 937 | usd_joint_dict = { 938 | "PhysicsPrismaticJoint": { 939 | "frictionloss": {"physxJoint:jointFriction"}, 940 | "damping": {"physxLimit:linear:damping"}, 941 | "stiffness": {"physxLimit:linear:stiffness"} 942 | }, 943 | "PhysicsRevoluteJoint": { 944 | "frictionloss": {"physxJoint:jointFriction"}, 945 | "damping": {"physxLimit:angular:damping"}, 946 | "stiffness": {"physxLimit:angular:stiffness"} 947 | }, 948 | "PhysicsSphericalJoint": { 949 | "frictionloss": {"physxJoint:jointFriction"}, 950 | "damping": { 951 | "physxLimit:transX:damping", 952 | "physxLimit:transY:damping", 953 | "physxLimit:transZ:damping" 954 | }, 955 | "stiffness": { 956 | "physxLimit:transX:stiffness", 957 | "physxLimit:transY:stiffness", 958 | "physxLimit:transZ:stiffness" 959 | } 960 | } 961 | } 962 | 963 | joint_type = joint.GetTypeName() 964 | if joint_type not in usd_joint_dict: 965 | print(f"set unsupported joint properties {joint_type}") 966 | return 967 | 968 | properties = usd_joint_dict[joint_type] 969 | for prop_name, usd_attrs in properties.items(): 970 | value = joint_data.get(prop_name) 971 | if not value: 972 | continue 973 | for attr in usd_attrs: 974 | if joint.HasAttribute(attr): 975 | joint.GetAttribute(attr).Set(float(value)) 976 | else: 977 | joint.CreateAttribute(attr, Sdf.ValueTypeNames.Float).Set(float(value)) 978 | 979 | def clear_temp_usd(root_dir): 980 | for dirpath, dirnames, filenames in os.walk(root_dir): 981 | for filename in filenames: 982 | file_path = os.path.join(dirpath, filename) 983 | name1, ext1 = os.path.splitext(filename) 984 | name2, ext2 = os.path.splitext(name1) 985 | if ext2 == '.tmp' and ext1 == '.usd': 986 | os.remove(file_path) 987 | 988 | @staticmethod 989 | def convert_name_from_mjcf_2_usd(mjcf_name): 990 | return mjcf_name.replace('.', '_').replace('-', '_') 991 | 992 | @staticmethod 993 | def convert_quat_mjcf_2_scipy(quat_mjcf): 994 | """Convert quaternion from MJCF (w, x, y, z) to SciPy (x, y, z, w) format.""" 995 | quat_scipy_list = quat_mjcf[1:] + [quat_mjcf[0]] 996 | return quat_scipy_list 997 | 998 | @staticmethod 999 | def convert_quat_scipy_2_mjcf(quat_scipy): 1000 | """Convert quaternion from SciPy (x, y, z, w) to MJCF (w, x, y, z)format.""" 1001 | quat_mjcf = [quat_scipy[3]] + list(quat_scipy[:3]) 1002 | return quat_mjcf 1003 | 1004 | @staticmethod 1005 | def get_xform_op(prim): 1006 | op_type_list = [] 1007 | op_name_list = [] 1008 | op_value_list = [] 1009 | op_pre_list = [] 1010 | 1011 | xform = UsdGeom.Xformable(prim) 1012 | 1013 | for op in xform.GetOrderedXformOps(): 1014 | op_type = op.GetOpType() 1015 | op_name = op.GetName() 1016 | value = op.Get() 1017 | precision = op.GetPrecision() 1018 | 1019 | op_type_list.append(op_type) 1020 | op_name_list.append(op_name) 1021 | op_value_list.append(value) 1022 | op_pre_list.append(precision) 1023 | 1024 | return op_type_list,op_name_list,op_value_list,op_pre_list 1025 | 1026 | @staticmethod 1027 | def set_xform_op(prim,op_type_list,op_name_list,op_value_list,op_pre_list): 1028 | tgt_xform = UsdGeom.Xformable(prim) 1029 | 1030 | for idx, op in enumerate(op_type_list): 1031 | op_type = op_type_list[idx] 1032 | op_pre = op_pre_list[idx] 1033 | op_name = op_name_list[idx] 1034 | opo_value = op_value_list[idx] 1035 | 1036 | tgt_op = tgt_xform.AddXformOp(op_type, op_pre, op_name) 1037 | tgt_op.Set(opo_value) 1038 | 1039 | @staticmethod 1040 | def clear_xform_ops(prim: Usd.Prim): 1041 | """Remove all xform ops from input prim. 1042 | 1043 | Args: 1044 | prim (Usd.Prim): The input USD prim. 1045 | """ 1046 | xformable = UsdGeom.Xformable(prim) 1047 | if xformable: 1048 | xformable.ClearXformOpOrder() 1049 | # Remove any authored transform properties 1050 | authored_prop_names = prim.GetAuthoredPropertyNames() 1051 | for prop_name in authored_prop_names: 1052 | if prop_name.startswith("xformOp:"): 1053 | prim.RemoveProperty(prop_name) 1054 | 1055 | @staticmethod 1056 | def copy_xform_op(source:Usd.Prim,traget:Usd.Prim, flag: bool): 1057 | clear_xform_ops(traget) 1058 | op_type_list,op_name_list,op_value_list,op_pre_list = get_xform_op(source) 1059 | if flag: 1060 | clear_xform_ops(source) 1061 | set_xform_op(traget,op_type_list,op_name_list,op_value_list,op_pre_list) 1062 | 1063 | def get_xmls(xml_dir): 1064 | """ 1065 | Get all XML files 1066 | Args: 1067 | xml_dir: Path to the folder containing XML files 1068 | Returns: 1069 | xmls: List of XML file paths 1070 | """ 1071 | xmls = [] 1072 | if os.path.isdir(xml_dir): 1073 | for root, dirs, files in os.walk(xml_dir): 1074 | for file in files: 1075 | if file.endswith(".xml"): 1076 | xmls.append(os.path.join(root, file)) 1077 | elif xml_dir.endswith(".xml"): 1078 | xmls.append(xml_dir) 1079 | return xmls 1080 | 1081 | 1082 | if __name__ == "__main__": 1083 | 1084 | xml_path = "/home/lightwheel/Documents/cf.s/mjcf/standard/model.xml" 1085 | mjcf_to_usd(xml_path,'',False) --------------------------------------------------------------------------------