Skip to content

Commit 4e79937

Browse files
authored
Add scene_prep utilities and update Isaac Sim launch scripts (#324)
* Add scene_prep utilities and update Isaac Sim launch scripts Add simulation/isaac-sim/utils/scene_prep.py with four helpers: - scale_stage_prim: apply uniform XYZ scale to a stage prim - add_colliders: recursively apply CollisionAPI to all mesh prims - add_dome_light: add/update a dome light (intensity=3500, exposure=-3) - save_scene_as_contained_usd: collect Nucleus assets into a local directory Update example_one and example_two launch scripts to use scene_prep for environment scaling, collision setup, and lighting instead of inline UsdLux calls. Fix two_drone_scene_import. Update write-isaac-sim-scene SKILL.md to reference scene_prep utilities and ignore prepare_scene.py in .gitignore. * updated docs
1 parent 6410c87 commit 4e79937

File tree

8 files changed

+569
-248
lines changed

8 files changed

+569
-248
lines changed

.agents/skills/write-isaac-sim-scene/SKILL.md

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,19 @@ import threading
118118
import signal
119119
import atexit
120120
import time
121+
122+
# Scene preparation utilities (scaling, collision, lighting, export)
123+
# NOTE: importlib is used instead of a normal import because Isaac Sim's
124+
# script runner does not reliably set __file__, making sys.path manipulation
125+
# fragile. Loading the module by absolute file path is the robust approach.
126+
import importlib.util as _ilu, os as _os
127+
_scene_prep_path = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "..", "utils", "scene_prep.py")
128+
_spec = _ilu.spec_from_file_location("scene_prep", _os.path.normpath(_scene_prep_path))
129+
_scene_prep = _ilu.module_from_spec(_spec); _spec.loader.exec_module(_scene_prep)
130+
scale_stage_prim = _scene_prep.scale_stage_prim
131+
add_colliders = _scene_prep.add_colliders
132+
add_dome_light = _scene_prep.add_dome_light
133+
save_scene_as_contained_usd = _scene_prep.save_scene_as_contained_usd
121134
```
122135

123136
### 4. Enable Required Extensions
@@ -176,7 +189,11 @@ class YourSceneApp:
176189

177190
# Load environment
178191
self.load_environment()
179-
192+
193+
# Prepare environment (scale, colliders, lighting)
194+
stage = omni.usd.get_context().get_stage()
195+
self._prepare_environment(stage)
196+
180197
# Spawn vehicles
181198
self.spawn_vehicles()
182199

@@ -200,25 +217,22 @@ class YourSceneApp:
200217
# Option 3: Load custom USD environment
201218
# stage = self.pg.load_environment("/path/to/your/environment.usd")
202219

203-
# Add lighting (if not in loaded environment)
204-
self._setup_lighting()
205-
206220
# Add obstacles or other static objects
207221
self._add_environment_objects()
208-
209-
def _setup_lighting(self):
210-
"""Configure scene lighting."""
211-
stage = omni.usd.get_context().get_stage()
212-
213-
# Create distant light (sun)
214-
distant_light = UsdLux.DistantLight.Define(stage, "/World/DistantLight")
215-
distant_light.CreateIntensityAttr(1000.0)
216-
distant_light.CreateAngleAttr(0.5)
217-
distant_light.AddOrientOp().Set(Gf.Quatf(0.7071, 0.7071, 0, 0)) # Point down
218-
219-
# Create ambient light
220-
dome_light = UsdLux.DomeLight.Define(stage, "/World/DomeLight")
221-
dome_light.CreateIntensityAttr(500.0)
222+
223+
def _prepare_environment(self, stage):
224+
"""Scale, add collisions, and light the environment."""
225+
stage_prim = stage.GetPrimAtPath("/World/stage")
226+
if stage_prim.IsValid():
227+
# STAGE_SCALE: use 0.01 for Nucleus assets authored in cm, 1.0 if already in meters
228+
scale_stage_prim(stage, "/World/stage", STAGE_SCALE)
229+
add_colliders(stage_prim)
230+
# Allow physics to settle after adding colliders
231+
for _ in range(10):
232+
omni.kit.app.get_app().update()
233+
# add_dome_light defaults: intensity=3500, exposure=-3
234+
# Override via kwargs, e.g. add_dome_light(stage, intensity=5000, exposure=-2)
235+
add_dome_light(stage)
222236

223237
def _add_environment_objects(self):
224238
"""Add obstacles or other objects to the environment."""
@@ -498,6 +512,27 @@ Any limitations or known problems.
498512

499513
## Advanced Topics
500514

515+
### Scene Preparation Utilities
516+
517+
**File:** `simulation/isaac-sim/utils/scene_prep.py`
518+
519+
Four reusable helpers that cover the most common environment setup tasks. Import them as shown in Step 3.
520+
521+
| Function | When to use |
522+
|----------|-------------|
523+
| `scale_stage_prim(stage, prim_path, scale)` | Nucleus assets authored in centimetres need `STAGE_SCALE=0.01`; assets already in metres use `1.0`. |
524+
| `add_colliders(stage_prim)` | **Must** be called for physics to interact with environment meshes. Without it drones fall through the floor. Call after scaling. |
525+
| `add_dome_light(stage, **kwargs)` | Adds uniform hemisphere lighting. Defaults: `intensity=3500`, `exposure=-3`. Pass kwargs to override, e.g. `add_dome_light(stage, intensity=5000)`. |
526+
| `save_scene_as_contained_usd(src_url, output_dir)` | Copies a Nucleus-hosted stage (and all its textures/MDLs) to a local directory using `omni.kit.usd.collect.Collector`. Useful for archiving or offline replay. |
527+
528+
**Two-step save pattern** used internally by `save_scene_as_contained_usd`:
529+
1. `export_as_stage_async` — writes a flat `.usd` of the live stage
530+
2. `Collector` — resolves and copies all referenced Nucleus assets locally
531+
532+
Set `SAVE_SCENE_TO = None` in your script to skip saving entirely.
533+
534+
---
535+
501536
### Multi-Robot Scenarios
502537

503538
For multiple robots, spawn additional vehicles with unique IDs and ports:
@@ -578,6 +613,10 @@ def _add_dynamic_obstacle(self):
578613
- ✅ Position sensors outside vehicle collision geometry
579614
- ✅ Typical camera position: forward of vehicle center
580615

616+
### Missing Colliders on Environment Meshes
617+
- ❌ Loading a Nucleus environment without calling `add_colliders()`
618+
- ✅ Call `add_colliders(stage_prim)` after scaling — drones will fall through the floor otherwise
619+
581620
### World Reset
582621
-**Not calling world.reset()**
583622
- ✅ Call world.reset() after adding all objects before stepping
@@ -626,6 +665,9 @@ docker exec airstack-isaac-sim-1 bash -c "ros2 topic hz /drone1/sensors/camera/i
626665
- Single drone: `simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_launch_script.py`
627666
- Two drones: `simulation/isaac-sim/launch_scripts/example_two_px4_pegasus_launch_script.py`
628667

668+
- **Scene Preparation Utilities:**
669+
- `simulation/isaac-sim/utils/scene_prep.py`
670+
629671
- **Related Skills:**
630672
- [test-in-simulation](../test-in-simulation) - Testing modules in Isaac Sim
631673
- [debug-module](../debug-module) - Debugging simulation issues

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,5 @@ nucleus_token.txt
6464
# mkdocs
6565
mkdocs.log
6666

67-
prompts/
67+
prompts/
68+
simulation/isaac-sim/launch_scripts/prepare_scene.py

docs/simulation/isaac_sim/pegasus_scene_setup.md

Lines changed: 62 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -41,35 +41,75 @@ This is toggled by the `ISAAC_SIM_USE_STANDALONE` variable in the `.env` file. I
4141

4242
## Scripted Scene Generation
4343

44-
Example scripts for generating a scene are provided in `AirStack/simulation/isaac-sim/launch_scripts/`
45-
You can also write your own scripts for your own custom scenarios.
44+
Python scripts give full programmatic control over every aspect of scene construction at startup, making them especially useful for:
4645

47-
When using the scripted launch method:
46+
- **Reproducible scenarios** — the entire scene is defined in code, not a saved USD file.
47+
- **Nucleus asset integration** — loading `omniverse://` assets that need unit conversion or collision baking before use.
48+
- **Multi-robot setups** — dynamically spawning N drones with distinct IDs and positions.
49+
- **CI / headless testing** — running without a GUI.
50+
51+
Example scripts are provided in `simulation/isaac-sim/launch_scripts/`.
52+
53+
### Scene Preparation Utilities
54+
55+
**Location:** `simulation/isaac-sim/utils/scene_prep.py`
56+
57+
`scene_prep.py` provides helpers that are shared across all example launch scripts:
58+
59+
| Function | Purpose |
60+
|----------|---------|
61+
| `scale_stage_prim(stage, prim_path, scale_factor)` | Applies a uniform XYZ scale transform to the prim at `prim_path`, clearing any existing xform ops first. Use `0.01` for Nucleus assets authored in centimetres; use `1.0` for assets already in metres. |
62+
| `add_colliders(stage_prim)` | Recursively walks every child of `stage_prim` and applies `UsdPhysics.CollisionAPI` to each `UsdGeom.Mesh`. **Must be called or drones fall through the floor.** Skips prims that already have the API. |
63+
| `add_dome_light(stage, intensity=3500, exposure=-3)` | Adds a hemisphere light at `/World/DomeLight` (or updates it if it already exists). Pass `intensity` / `exposure` keyword arguments to override the defaults. |
64+
| `save_scene_as_contained_usd(source_usd_url, output_dir)` | Copies the stage and all its dependencies (textures, MDL materials) from a Nucleus `omniverse://` URL into a local directory via `omni.kit.usd.collect.Collector`. Set `SAVE_SCENE_TO = None` in your script to skip this step. |
65+
| `get_stage_meters_per_unit(stage)` | Returns `(meters_per_unit, scene_scale_factor)`. Multiply metric coordinates by `scene_scale_factor` to convert them into stage-space units. Useful for computing drone spawn heights when `STAGE_SCALE != 1.0`. |
66+
67+
#### Loading `scene_prep`
68+
69+
`scene_prep.py` lives in `utils/`, which is not on `sys.path` by default. The example scripts add it at runtime before importing:
70+
71+
```python
72+
import sys
73+
import os
74+
75+
sys.path.insert(0, os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "utils")))
76+
import scene_prep
77+
from scene_prep import scale_stage_prim, add_colliders, add_dome_light, save_scene_as_contained_usd
78+
```
79+
80+
81+
82+
### Running a Standalone Script
83+
84+
Set the following variables in the top-level `.env` file and then launch as normal:
4885

49-
1. Define your desired script in the `.env` file and run
5086
```bash
87+
# .env
88+
ISAAC_SIM_USE_STANDALONE=true
89+
ISAAC_SIM_SCRIPT_NAME=example_one_px4_pegasus_launch_script.py
90+
```
91+
92+
```bash
93+
# Start Isaac Sim with the selected script
5194
airstack up isaac-sim
95+
5296
```
53-
This will start Isaac Sim and generate the drone(s) within the scene.
5497

55-
2. Stop the simulation by pressing the square “Stop” button on the left side of the Isaac Sim interface.
56-
This is necessary before editing the OmniGraph.
98+
Scripts must live in `simulation/isaac-sim/launch_scripts/`. Set `ISAAC_SIM_SCRIPT_NAME` to the filename only (no path).
5799

58-
![Stop Sim Button](pegasus_setup_images/stop_button.png)
100+
---
59101

60-
3. Open and connect the OmniGraph:
61-
- Right-click the graph and choose “Open Graph.”
62-
- Connect the ROS2Context node so that it feeds into all desired sensor subgraphs (e.g., cameras, LiDARs).
63-
![Connected Subgraphs](pegasus_setup_images/connected_sensor_subgraphs.png)
64-
> This manual step is temporarily required due to a known bug. Automation of this is planned in a future update.
102+
### Common Pitfalls
65103

66-
4. Save the scene and drones once everything is connected.
67-
- You can save both the stage and the drone USDs for future reuse.
68-
![Saving Scene](pegasus_setup_images/saving_scene.png)
104+
| Symptom | Cause | Fix |
105+
|---------|-------|-----|
106+
| Drone falls through the floor immediately | `add_colliders` was not called | Call `add_colliders(stage_prim)` after loading the environment |
107+
| Colliders applied to wrong-scale geometry | `add_colliders` called before `scale_stage_prim` | Always call `scale_stage_prim` first, then `add_colliders` |
108+
| Physics behaves erratically after colliders added | Update frames not pumped after `add_colliders` | Pump at least 10 `omni.kit.app.get_app().update()` calls after `add_colliders` |
109+
| `ImportError: No module named 'omni'` at script top | `omni` imported before `SimulationApp()` | Move all `omni.*` imports to after the `SimulationApp(...)` line |
110+
| `scene_prep` not found / `ModuleNotFoundError` | `utils/` not on `sys.path` in Isaac Sim's Python | Use `sys.path.insert` to add the `utils/` directory before importing `scene_prep` |
111+
| Drone spawns at wrong height in cm-scale scene | Spawn coordinates not converted to stage space | Multiply metric `init_pos` values by `scene_scale` from `get_stage_meters_per_unit` |
69112

70-
5. Update your environment variables:
71-
- Set the ISAAC_SIM_GUI variable to point to your newly saved .usd file (make sure to put the path within the docker container or in the omniverse server)
72-
- Set ISAAC_SIM_USE_STANDALONE to "false" to load this saved environment directly next time.
73113

74114
## Known bugs and workarounds for Scripted Scene Generation
75115

@@ -94,4 +134,6 @@ Occasionally, the right camera in a stereo camera pair may fail to initialize.
94134
The drone not arming/taking off can be a symptom of the PX4Multirotor Node not being recognized in omnigraph. This may be due the `pegasus.simulator` extension not being loaded.
95135

96136
- To fix, launch the simulator with `airstack up isaac-sim`, in the toolbar, click Window -> Extensions -> Third Party, serach for "pegasus", select the "PEGASUS SIMULATOR" and enable "AUTOLOAD"
97-
- Restart your docker container by running `airstack down isaac-sim && airstack up isaac-sim` and the extension should load every time now.
137+
- Restart your docker container by running `airstack down isaac-sim && airstack up isaac-sim` and the extension should load every time now.
138+
139+
---

0 commit comments

Comments
 (0)