fix: IS 4.5.0 -> 5.0.0 migration — USD metadata, DomeLight, scene reuse

- Fix USD metersPerUnit/upAxis for IS 5.0.0 (no longer auto-compensated)
- Batch fix all Aligned_obj.usd, table, and art USD files with backups
- Fix DomeLight rotation to Z-axis only (prevent tilted environment map)
- Fix scene reuse across episodes (arena_file caching, task clearing, prim guard)
- Add migration tools: scan_usd_metadata.py, fix_usd_metadata.py
- Add migration guide: migerate/migerate.md
- Add nvidia-curobo to .gitignore
- Fix sort_the_rubbish config: obj_0 -> obj_1 (obj_0 does not exist)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tangger
2026-04-03 11:10:39 +08:00
parent f338199bcb
commit 03d9a5b909
65 changed files with 862 additions and 6 deletions

View File

@@ -0,0 +1,159 @@
"""
Batch fix USD metadata for IS 5.0.0 compatibility.
IS 4.5.0 auto-added 'Rotate:unitsResolve' and 'Scale:unitsResolve' xformOps
to compensate for metersPerUnit/upAxis differences. IS 5.0.0 no longer does this,
causing RigidObject physics to break (objects fly away).
Fix: Change metersPerUnit to 1.0 and upAxis to Z in USD metadata.
Vertex data stays unchanged (already in correct scale for the scene).
Usage:
# Scan only (dry run):
python migerate/fix_usd_metadata.py --root workflows/simbox/example_assets --dry-run
# Fix all:
python migerate/fix_usd_metadata.py --root workflows/simbox/example_assets
# Fix specific pattern:
python migerate/fix_usd_metadata.py --root workflows/simbox/example_assets --pattern "Aligned_obj.usd"
# Skip specific directories (e.g. robot models, curobo assets):
python migerate/fix_usd_metadata.py --root . --skip curobo,robot
# Restore from backups:
python migerate/fix_usd_metadata.py --root workflows/simbox/example_assets --restore
"""
import argparse
import glob
import os
import shutil
import sys
from pxr import Usd, UsdGeom
def fix_usd_files(root: str, pattern: str = "*.usd", target_mpu: float = 1.0,
target_up: str = "Z", dry_run: bool = False, skip: list = None):
usd_files = glob.glob(os.path.join(root, "**", pattern), recursive=True)
usd_files = [f for f in usd_files if not f.endswith(".bak")]
usd_files.sort()
skip = skip or []
print(f"Scanning {len(usd_files)} USD files under: {root}")
if dry_run:
print("[DRY RUN] No files will be modified.\n")
print()
fixed = 0
skipped = 0
skipped_dir = 0
errors = 0
for fpath in usd_files:
rel = os.path.relpath(fpath, root)
# Check skip patterns
if any(s in rel for s in skip):
print(f" SKIP (dir filter): {rel}")
skipped_dir += 1
continue
try:
stage = Usd.Stage.Open(fpath)
if stage is None:
print(f" ERROR: Cannot open {rel}")
errors += 1
continue
mpu = UsdGeom.GetStageMetersPerUnit(stage)
up = UsdGeom.GetStageUpAxis(stage)
needs_fix = (mpu != target_mpu) or (up != target_up)
if not needs_fix:
print(f" OK: {rel} (mpu={mpu}, up={up})")
skipped += 1
continue
if dry_run:
print(f" WOULD FIX: {rel} (mpu={mpu}, up={up} -> mpu={target_mpu}, up={target_up})")
fixed += 1
continue
# Backup
bak = fpath + ".bak"
if not os.path.exists(bak):
shutil.copy2(fpath, bak)
# Fix
UsdGeom.SetStageMetersPerUnit(stage, target_mpu)
UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.z if target_up == "Z" else UsdGeom.Tokens.y)
stage.GetRootLayer().Save()
print(f" FIXED: {rel} (mpu={mpu}/{up} -> {target_mpu}/{target_up})")
fixed += 1
except Exception as e:
print(f" ERROR: {rel} -> {e}")
errors += 1
print(f"\nDone: {fixed} {'would fix' if dry_run else 'fixed'}, {skipped} already OK, "
f"{skipped_dir} skipped (dir filter), {errors} errors.")
if not dry_run and fixed > 0:
print("Backups saved as *.bak.")
print("To restore: python migerate/fix_usd_metadata.py --root <DIR> --restore")
return fixed
def restore_backups(root: str, pattern: str = "*.usd"):
bak_files = glob.glob(os.path.join(root, "**", pattern + ".bak"), recursive=True)
bak_files.sort()
if not bak_files:
print(f"No .bak files found under: {root}")
return
print(f"Found {len(bak_files)} backup files. Restoring...")
for bak in bak_files:
original = bak[:-4] # remove .bak
shutil.copy2(bak, original)
print(f" Restored: {os.path.relpath(original, root)}")
print(f"\nDone: {len(bak_files)} files restored.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Batch fix USD metersPerUnit/upAxis metadata for IS 5.0.0 compatibility",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Dry run on example assets:
python migerate/fix_usd_metadata.py --root workflows/simbox/example_assets --dry-run
# Fix all Aligned_obj.usd in example assets:
python migerate/fix_usd_metadata.py --root workflows/simbox/example_assets --pattern "Aligned_obj.usd"
# Fix everything except robot/curobo dirs:
python migerate/fix_usd_metadata.py --root . --skip curobo,robot
# Restore all backups:
python migerate/fix_usd_metadata.py --root workflows/simbox/example_assets --restore
""")
parser.add_argument("--root", default="workflows/simbox/example_assets",
help="Root directory to scan (default: workflows/simbox/example_assets)")
parser.add_argument("--pattern", default="*.usd", help="Filename glob pattern (default: *.usd)")
parser.add_argument("--target-mpu", type=float, default=1.0, help="Target metersPerUnit (default: 1.0)")
parser.add_argument("--target-up", choices=["Y", "Z"], default="Z", help="Target upAxis (default: Z)")
parser.add_argument("--dry-run", action="store_true", help="Scan and report only, do not modify files")
parser.add_argument("--skip", default="", help="Comma-separated dir substrings to skip (e.g. 'curobo,robot')")
parser.add_argument("--restore", action="store_true", help="Restore all .bak files to originals")
args = parser.parse_args()
if args.restore:
restore_backups(args.root, args.pattern)
else:
skip_list = [s.strip() for s in args.skip.split(",") if s.strip()]
fix_usd_files(args.root, args.pattern, args.target_mpu, args.target_up, args.dry_run, skip_list)

199
migerate/migerate.md Normal file
View File

@@ -0,0 +1,199 @@
# Isaac Sim 4.5.0 -> 5.0.0 Migration Guide
## Background
- GPU: NVIDIA RTX PRO 6000 Blackwell (SM_120, compute capability 12.0)
- IS 4.5.0 = Kit 106.5.0, DLSS Ray Reconstruction not supported on Blackwell (needs Kit >= 106.5.3), rendering has noise
- IS 5.0.0 = Kit 107.x, fixes Blackwell rendering noise
- IS 5.1.0 headless camera rendering completely broken (known bug GitHub #3250), abandoned
## Issues & Solutions
### 1. SimulationApp import path changed
**Error:** `ModuleNotFoundError: No module named 'omni.isaac.kit'`
**Cause:** IS 5.x changed the import path.
**Fix** (`nimbus_extension/components/load/env_loader.py`):
```python
try:
from isaacsim import SimulationApp # IS 5.x
except ImportError:
from omni.isaac.kit import SimulationApp # IS 4.x
```
### 2. USD metersPerUnit / upAxis not auto-compensated
**Error:** RigidObject Z coordinate flying to 109.75+ after `world.reset()`, curobo `plan_single()` fails with "Plan did not converge" (goal_pos Z = 25261 meters).
**Cause:** IS 4.5.0 auto-added `Rotate:unitsResolve` (X=90) and `Scale:unitsResolve` (0.01, 0.01, 0.01) xformOps to compensate for USD files with `metersPerUnit=0.01, upAxis=Y`. IS 5.0.0 no longer does this, causing PhysX to misinterpret RigidObject positions.
GeometryObjects (no physics) were unaffected — only RigidObjects with PhysX simulation had the issue.
**Fix:** Change USD metadata directly using pxr scripting. Run `fix_usd_metadata.py`:
```python
from pxr import Usd, UsdGeom
stage = Usd.Stage.Open(usd_path)
UsdGeom.SetStageMetersPerUnit(stage, 1.0) # was 0.01
UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.z) # was Y
stage.GetRootLayer().Save()
```
All 14 `Aligned_obj.usd` files under `workflows/simbox/example_assets` have been batch-fixed. Backups saved as `.bak`.
**Verification:** After fix, pick_object_right Z went from 109.75 to 0.7968 (normal).
### 3. scipy `scalar_first` parameter not supported
**Error:** `Rotation.as_quat() takes no keyword arguments`
**Cause:** IS 5.0.0 ships scipy < 1.11 which doesn't support `scalar_first` parameter in `Rotation.from_quat()` / `Rotation.as_quat()`. Dozens of files use this parameter.
**Fix** (`launcher.py`): Monkey-patch via subclass at startup:
```python
try:
from scipy.spatial.transform import Rotation as _R
_R.from_quat([1, 0, 0, 0], scalar_first=True)
except TypeError:
# Install Rotation subclass that handles scalar_first
# See launcher.py for full implementation
pass
```
### 4. `arena_file_path` is None on second reset
**Error:** `arena_file_path` becomes None after first episode.
**Cause:** `self.task_cfg.pop("arena_file", None)` on line 107 of `simbox_dual_workflow.py` deletes the key after first use.
**Fix** (`workflows/simbox_dual_workflow.py`): Cache the value:
```python
self._arena_file_path = None # in __init__
# in reset():
arena_file_path = self.task_cfg.get("arena_file", None) or self._arena_file_path
if arena_file_path:
self._arena_file_path = arena_file_path
```
### 5. "Task name should be unique in the world"
**Error:** `AssertionError` when calling `world.add_task()` on second episode.
**Cause:** World retains tasks from previous episode.
**Fix** (`workflows/simbox_dual_workflow.py`):
```python
self.world._current_tasks.clear()
self.world.add_task(self.task)
```
### 6. "A prim already exists at prim path"
**Error:** Scene setup tries to re-create existing prims on repeated `world.reset()` calls.
**Fix** (`workflows/simbox/core/tasks/banana.py`): Add `_scene_initialized` guard:
```python
def set_up_scene(self, scene):
if getattr(self, '_scene_initialized', False):
self._task_objects = {}
self._task_objects |= (
getattr(self, 'fixtures', {}) |
getattr(self, 'objects', {}) |
getattr(self, 'robots', {}) |
getattr(self, 'cameras', {})
)
return
self._scene_initialized = True
super().set_up_scene(scene)
```
### 7. Collision group prim already exists
**Error:** Collision group prims persist across resets.
**Fix** (`workflows/simbox/core/utils/collision_utils.py`): Remove existing collision prims before re-creating:
```python
collision_prim = stage.GetPrimAtPath(collision_root_path)
if collision_prim.IsValid():
stage.RemovePrim(collision_root_path)
```
### 8. DomeLight environment map tilted
**Error:** HDR environment map appears rotated/tilted in viewport and rendered output.
**Cause:** DomeLight rotation was randomized on all 3 axes (X, Y, Z). Rotating X/Y tilts the environment.
**Fix** (`workflows/simbox/core/tasks/banana.py`): Only rotate Z axis:
```python
# Before:
rotation = [random.uniform(r[0], r[1]) for _ in range(3)]
# After:
rotation = [0.0, 0.0, random.uniform(r[0], r[1])]
```
## Files Modified
| File | Change |
|------|--------|
| `nimbus_extension/components/load/env_loader.py` | SimulationApp import compatibility |
| `launcher.py` | scipy Rotation monkey-patch |
| `workflows/simbox_dual_workflow.py` | arena_file caching, task clearing, task reuse |
| `workflows/simbox/core/tasks/banana.py` | Scene re-init guard, DomeLight rotation fix |
| `workflows/simbox/core/utils/collision_utils.py` | Collision group cleanup |
| `workflows/simbox/example_assets/**/Aligned_obj.usd` | metersPerUnit=1.0, upAxis=Z |
| `fix_usd_metadata.py` | Batch USD metadata fix script |
## Tools
Migration tools are located in the `migerate/` directory.
### scan_usd_metadata.py — Scan USD metadata
Scan all USD files and report their `metersPerUnit` / `upAxis`:
```bash
conda activate banana500
# Scan entire project
python migerate/scan_usd_metadata.py --root .
# Scan specific directory
python migerate/scan_usd_metadata.py --root workflows/simbox/example_assets
```
Exit code: 0 = all OK, 1 = found files with non-standard metadata.
建议扫描后丢给大模型分析一下然后再做下一步
### fix_usd_metadata.py — Batch fix USD metadata
Fix `metersPerUnit` and `upAxis` in USD files, with backup/restore support:
```bash
conda activate banana500
# Dry run — see what would be fixed, no changes made
python migerate/fix_usd_metadata.py --root . --dry-run --skip curobo,robot
# Fix example assets only (default)
python migerate/fix_usd_metadata.py --root workflows/simbox/example_assets
# Fix all USD in project, skip robot/curobo models
python migerate/fix_usd_metadata.py --root . --skip curobo,robot
# Fix only Aligned_obj.usd files
python migerate/fix_usd_metadata.py --root . --pattern "Aligned_obj.usd"
# Restore from backups (undo all fixes)
python migerate/fix_usd_metadata.py --root workflows/simbox/example_assets --restore
```
**Important:** Do NOT fix robot USD (curobo, split_aloha) their metersPerUnit/upAxis are intentionally set for their own coordinate systems. Use `--skip` to exclude them.
## Notes
- `plan_single()` occasionally returns None for edge-case target poses (workspace boundary). This is normal and happens in IS 4.5.0 too.
- Example HDR is only 1k resolution production should use 4k/8k for better background quality.
- Example `envmap_lib` only has 1 HDR file add more for randomization diversity.
- `non_recyclable_garbage/obj_0` does not exist in example assets (only obj_1~obj_11). Config changed to use `obj_1`.

View File

@@ -0,0 +1,70 @@
"""
Scan all USD files in the project and report their metersPerUnit / upAxis metadata.
Usage:
python migerate/scan_usd_metadata.py [--root DIR]
Default root: current working directory (project root).
"""
import argparse
import glob
import os
import sys
from pxr import Usd, UsdGeom
def scan(root: str, target_mpu: float = 1.0, target_up: str = "Z"):
usd_files = glob.glob(os.path.join(root, "**", "*.usd"), recursive=True)
usd_files = [f for f in usd_files if not f.endswith(".bak")]
usd_files.sort()
ok_files = []
diff_files = []
error_files = []
for fpath in usd_files:
try:
stage = Usd.Stage.Open(fpath)
if stage is None:
error_files.append((fpath, "Cannot open stage"))
continue
mpu = UsdGeom.GetStageMetersPerUnit(stage)
up = UsdGeom.GetStageUpAxis(stage)
rel = os.path.relpath(fpath, root)
if mpu == target_mpu and up == target_up:
ok_files.append((rel, mpu, up))
else:
diff_files.append((rel, mpu, up))
except Exception as e:
error_files.append((fpath, str(e)))
# Report
print(f"Scanned {len(ok_files) + len(diff_files) + len(error_files)} USD files under: {root}")
print(f" OK (mpu={target_mpu}, up={target_up}): {len(ok_files)}")
print(f" Different: {len(diff_files)}")
print(f" Errors: {len(error_files)}")
if diff_files:
print(f"\n{'='*80}")
print(f"Files with non-standard metadata (mpu != {target_mpu} or up != {target_up}):")
print(f"{'='*80}")
for rel, mpu, up in diff_files:
print(f" mpu={mpu:<8} up={up:<4} {rel}")
if error_files:
print(f"\n{'='*80}")
print("Files with errors:")
print(f"{'='*80}")
for fpath, err in error_files:
print(f" ERROR: {fpath} -> {err}")
return diff_files
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Scan USD files for metersPerUnit/upAxis metadata")
parser.add_argument("--root", default=os.getcwd(), help="Root directory to scan (default: cwd)")
args = parser.parse_args()
diff = scan(args.root)
sys.exit(1 if diff else 0)