| name | write-isaac-sim-scene |
| description | Create custom simulation environments in Isaac Sim using standalone Python scripts with Pegasus extension. Use when creating test scenarios, multi-robot simulations, or custom environments for testing autonomy modules. |
| license | Apache-2.0 |
| metadata | {"author":"AirLab CMU","repository":"AirStack"} |
Skill: Write Isaac Sim Scene in Standalone Python Mode
When to Use
Creating custom simulation environments for testing autonomy modules, multi-robot scenarios, or specific environmental conditions.
Prerequisites
- Isaac Sim container running or accessible
- Understanding of Pegasus Simulator extension for drones
- Knowledge of required sensors and vehicle configuration
- Familiarity with Python and basic Isaac Sim concepts
Isaac Sim Integration Overview
AirStack uses NVIDIA Isaac Sim with the Pegasus Simulator extension for high-fidelity drone simulation. There are two ways to define scenes:
- USD Files: Static scene description files (
.usd format)
- Standalone Python Scripts: Dynamic scene creation with full programmatic control (recommended for complex scenarios)
This skill covers standalone Python mode.
Script Structure Overview
Standalone Python scripts follow this pattern:
1. Start SimulationApp (BEFORE any omni imports)
2. Import required modules
3. Enable necessary extensions
4. Create PegasusApp class
- Initialize Pegasus interface
- Load environment
- Spawn vehicles with sensors
- Setup physics and backends
5. Run simulation loop
6. Clean up
Steps
1. Create Script File
Location: simulation/isaac-sim/launch_scripts/<your_scene_name>.py
cd simulation/isaac-sim/launch_scripts/
touch your_scene_name.py
chmod +x your_scene_name.py
2. Script Header and SimulationApp Initialization
Critical: SimulationApp MUST be started before importing any omni modules.
"""
Description: Brief description of your simulation scene
Author: Your Name
Date: YYYY-MM-DD
This script creates a simulation environment for testing <specific feature>.
- Number of drones: X
- Sensors: Camera, LiDAR, etc.
- Environment: Description
"""
import carb
from isaacsim import SimulationApp
simulation_app = SimulationApp({"headless": False})
import rclpy
print(f"[Launcher] SUCCESS: rclpy imported from {rclpy.__file__}")
3. Import Required Modules
import omni.kit.app
import omni.timeline
import omni.ui
from omni.isaac.core.world import World
from datetime import datetime
from pxr import UsdLux, Gf, UsdGeom
from pegasus.simulator.params import SIMULATION_ENVIRONMENTS, ROBOTS
from pegasus.simulator.logic.interface.pegasus_interface import PegasusInterface
from pegasus.simulator.ogn.api.spawn_multirotor import spawn_px4_multirotor_node
from pegasus.simulator.ogn.api.spawn_zed_camera import add_zed_stereo_camera_subgraph
from pegasus.simulator.ogn.api.spawn_rtx_lidar import add_rtx_lidar_subgraph
from pegasus.simulator.logic.vehicles.multirotor import Multirotor, MultirotorConfig
from pegasus.simulator.logic.state import State
from pegasus.simulator.logic.backends.px4_mavlink_backend import (
PX4MavlinkBackend,
PX4MavlinkBackendConfig
)
from pegasus.simulator.logic.backends.ros2_backend import ROS2Backend
from scipy.spatial.transform import Rotation
import numpy as np
import os
import subprocess
import threading
import signal
import atexit
import time
import importlib.util as _ilu, os as _os
_scene_prep_path = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), "..", "utils", "scene_prep.py")
_spec = _ilu.spec_from_file_location("scene_prep", _os.path.normpath(_scene_prep_path))
_scene_prep = _ilu.module_from_spec(_spec); _spec.loader.exec_module(_scene_prep)
scale_stage_prim = _scene_prep.scale_stage_prim
add_colliders = _scene_prep.add_colliders
add_dome_light = _scene_prep.add_dome_light
save_scene_as_contained_usd = _scene_prep.save_scene_as_contained_usd
4. Enable Required Extensions
ext_manager = omni.kit.app.get_app().get_extension_manager()
required_extensions = [
"omni.graph.core",
"omni.graph.action",
"omni.graph.action_nodes",
"isaacsim.core.nodes",
"omni.graph.ui",
"omni.graph.visualization.nodes",
"omni.graph.scriptnode",
"omni.graph.window.action",
"omni.graph.window.generic",
"omni.graph.ui_nodes",
"pegasus.simulator",
]
for ext in required_extensions:
if not ext_manager.is_extension_enabled(ext):
print(f"[Launcher] Enabling extension: {ext}")
ext_manager.set_extension_enabled_immediate(ext, True)
print(f"[Launcher] Successfully enabled extension: {ext}")
else:
print(f"[Launcher] Extension already enabled: {ext}")
5. Create PegasusApp Class
class YourSceneApp:
"""
Simulation application for your specific scenario.
"""
def __init__(self):
print("[YourScene] Initializing simulation...")
self.pg = PegasusInterface()
self.world = World(**self.pg.world_settings)
self.pg.world = self.world
self.vehicles = {}
self.px4_processes = []
self.load_environment()
stage = omni.usd.get_context().get_stage()
self._prepare_environment(stage)
self.spawn_vehicles()
self.world.reset()
print("[YourScene] Simulation initialized successfully")
def load_environment(self):
"""Load or create the simulation environment."""
print("[YourScene] Loading environment...")
stage = self.pg.load_environment(SIMULATION_ENVIRONMENTS["Grid"]["usd"])
self._add_environment_objects()
def _prepare_environment(self, stage):
"""Scale, add collisions, and light the environment."""
stage_prim = stage.GetPrimAtPath("/World/stage")
if stage_prim.IsValid():
scale_stage_prim(stage, "/World/stage", STAGE_SCALE)
add_colliders(stage_prim)
for _ in range(10):
omni.kit.app.get_app().update()
add_dome_light(stage)
def _add_environment_objects(self):
"""Add obstacles or other objects to the environment."""
stage = omni.usd.get_context().get_stage()
pass
def spawn_vehicles(self):
"""Spawn drone vehicles with sensors and backends."""
print("[YourScene] Spawning vehicles...")
self._spawn_vehicle(
vehicle_id=0,
vehicle_name="drone1",
position=[0.0, 0.0, 1.0],
orientation=[0.0, 0.0, 0.0, 1.0],
px4_autostart_id=4001,
mavlink_tcp_port=4560,
px4_instance=0,
sensors={
"camera": True,
"lidar": False
}
)
def _spawn_vehicle(self, vehicle_id, vehicle_name, position, orientation,
px4_autostart_id, mavlink_tcp_port, px4_instance,
sensors=None):
"""
Spawn a single vehicle with specified configuration.
Args:
vehicle_id: Unique vehicle ID
vehicle_name: Name for the vehicle
position: [x, y, z] spawn position
orientation: [x, y, z, w] quaternion orientation
px4_autostart_id: PX4 vehicle type ID
mavlink_tcp_port: MAVLink TCP port for PX4 communication
px4_instance: PX4 instance number
sensors: Dict of sensors to add {"camera": bool, "lidar": bool}
"""
if sensors is None:
sensors = {"camera": True, "lidar": False}
config = MultirotorConfig()
px4_backend_config = PX4MavlinkBackendConfig({
"vehicle_id": vehicle_id,
"px4_autostart": px4_autostart_id,
"px4_dir": os.environ.get("PX4_DIR", "/PX4-Autopilot"),
"px4_instance": px4_instance,
"mavlink_tcp_port": mavlink_tcp_port,
"enable_lockstep": True,
"update_rate": 250.0
})
ros2_backend = ROS2Backend(
vehicle_id=vehicle_id,
config={
"namespace": vehicle_name,
"pub_sensors": True,
"pub_state": True
}
)
config.backends = [
PX4MavlinkBackend(px4_backend_config),
ros2_backend
]
vehicle = Multirotor(
stage_prefix="/World",
prim_path=f"/World/{vehicle_name}",
name=vehicle_name,
usd_model=ROBOTS["Iris"]["usd"],
init_pos=position,
init_orientation=orientation,
config=config
)
if sensors.get("camera", False):
self._add_camera_sensor(vehicle)
self.world.scene.add(vehicle)
self.vehicles[vehicle_name] = vehicle
print(f"[YourScene] Spawned vehicle: {vehicle_name}")
def _add_camera_sensor(self, vehicle):
"""Add stereo camera to vehicle."""
add_zed_stereo_camera_subgraph(
camera_prim_path=vehicle.prim_path + "/ZedCamera",
parent_prim_path=vehicle.prim_path,
config={
"graph_evaluator": "execution",
"resolution": (1280, 720),
"position": (0.3, 0.0, -0.1),
"orientation": (0.0, 0.0, 0.0, 1.0),
}
)
def _add_lidar_sensor(self, vehicle, graph_handle):
"""Add RTX LiDAR (OmniGraph subgraph) to vehicle."""
add_rtx_lidar_subgraph(
parent_graph_handle=graph_handle,
drone_prim=vehicle.prim_path,
robot_name="robot_1",
lidar_config="ouster_os1",
lidar_offset=[0.0, 0.0, 0.025],
lidar_rotation_offset=[0.0, 0.0, 0.0],
min_range=0.75,
)
def run(self):
"""Main simulation loop."""
print("[YourScene] Starting simulation loop...")
step_count = 0
while simulation_app.is_running():
self.world.step(render=True)
if step_count % 100 == 0:
pass
step_count += 1
print("[YourScene] Simulation loop ended")
def cleanup(self):
"""Clean up resources."""
print("[YourScene] Cleaning up...")
for process in self.px4_processes:
if process.poll() is None:
process.terminate()
process.wait()
self.px4_processes.clear()
6. Main Entry Point
def main():
"""Main entry point for the simulation."""
try:
app = YourSceneApp()
app.run()
except Exception as e:
print(f"[YourScene] Error: {e}")
import traceback
traceback.print_exc()
finally:
if 'app' in locals():
app.cleanup()
simulation_app.close()
if __name__ == "__main__":
main()
7. Configure in .env
Update the main .env file to use your script:
ISAAC_SIM_USE_STANDALONE="true"
ISAAC_SIM_SCRIPT_NAME="your_scene_name.py"
Alternatively, override from command line:
ISAAC_SIM_USE_STANDALONE=true ISAAC_SIM_SCRIPT_NAME="your_scene_name.py" airstack up isaac-sim
8. Test the Scene
Launch Isaac Sim with your script:
airstack up isaac-sim
airstack logs isaac-sim
airstack connect isaac-sim
9. Document the Scene
Create a README.md next to your script:
File: simulation/isaac-sim/launch_scripts/your_scene_name.md
# Your Scene Name
## Overview
Brief description of the simulation scene.
## Purpose
Why this scene was created and what it tests.
## Configuration
### Vehicles
- Number of drones: X
- Vehicle types: Quadrotor, fixed-wing, etc.
- Initial positions: List positions
### Sensors
- Cameras: Resolution, FoV
- LiDAR: Model, range
- Other sensors
### Environment
Description of the environment, obstacles, lighting.
## Usage
```bash
# Launch scene
ISAAC_SIM_SCRIPT_NAME="your_scene_name.py" airstack up isaac-sim
# With robot autonomy
airstack up isaac-sim robot
Parameters
Any configurable parameters in the script.
Known Issues
Any limitations or known problems.
## Advanced Topics
### Scene Preparation Utilities
**File:** `simulation/isaac-sim/utils/scene_prep.py`
Four reusable helpers that cover the most common environment setup tasks. Import them as shown in Step 3.
| Function | When to use |
|----------|-------------|
| `scale_stage_prim(stage, prim_path, scale)` | Nucleus assets authored in centimeters need `STAGE_SCALE=0.01`; assets already in meters use `1.0`. |
| `add_colliders(stage_prim)` | **Must** be called for physics to interact with environment meshes. Without it drones fall through the floor. Call after scaling. |
| `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)`. |
| `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. |
**Two-step save pattern** used internally by `save_scene_as_contained_usd`:
1. `export_as_stage_async` — writes a flat `.usd` of the live stage
2. `Collector` — resolves and copies all referenced Nucleus assets locally
Set `SAVE_SCENE_TO = None` in your script to skip saving entirely.
---
### Multi-Robot Scenarios
For multiple robots, spawn additional vehicles with unique IDs and ports:
```python
def spawn_vehicles(self):
for i in range(num_robots):
self._spawn_vehicle(
vehicle_id=i,
vehicle_name=f"drone{i}",
position=[i * 5.0, 0.0, 1.0], # Space them out
orientation=[0.0, 0.0, 0.0, 1.0],
px4_autostart_id=4001,
mavlink_tcp_port=4560 + i, # Unique port per vehicle
px4_instance=i,
sensors={"camera": True, "lidar": False}
)
Custom Sensor Configuration
Create custom sensor configurations:
def _add_custom_camera(self, vehicle, config):
"""Add camera with custom parameters."""
add_zed_stereo_camera_subgraph(
camera_prim_path=vehicle.prim_path + "/CustomCamera",
parent_prim_path=vehicle.prim_path,
config={
"resolution": config.get("resolution", (1920, 1080)),
"horizontal_fov": config.get("fov", 90.0),
"position": config.get("position", (0.3, 0.0, 0.0)),
"orientation": config.get("orientation", (0.0, 0.0, 0.0, 1.0)),
}
)
Dynamic Obstacles
Add moving obstacles:
def _add_dynamic_obstacle(self):
"""Add a moving obstacle to the scene."""
from omni.isaac.core.objects import DynamicCuboid
obstacle = DynamicCuboid(
prim_path="/World/DynamicObstacle",
position=[10.0, 0.0, 1.0],
scale=[1.0, 1.0, 1.0],
color=[1.0, 0.0, 0.0]
)
self.world.scene.add(obstacle)
Common Pitfalls
SimulationApp Import Order
- ❌ Importing omni modules before SimulationApp
- ✅ ALWAYS create SimulationApp first, then import omni modules
Extension Loading
- ❌ Missing required extensions
- ✅ Enable all required extensions before using their features
- ✅ Check extension status with
ext_manager.is_extension_enabled()
PX4 Port Conflicts
- ❌ Using same MAVLink port for multiple vehicles
- ✅ Each vehicle needs unique mavlink_tcp_port
- ✅ Increment port number for each vehicle: 4560, 4561, 4562, ...
Sensor Configuration
- ❌ Incorrect sensor placement (inside vehicle mesh)
- ✅ Position sensors outside vehicle collision geometry
- ✅ Typical camera position: forward of vehicle center
Missing Colliders on Environment Meshes
- ❌ Loading a Nucleus environment without calling
add_colliders()
- ✅ Call
add_colliders(stage_prim) after scaling — drones will fall through the floor otherwise
World Reset
- ❌ Not calling world.reset()
- ✅ Call world.reset() after adding all objects before stepping
Debugging
View Scene in GUI
Run with headless=False to see the scene:
simulation_app = SimulationApp({"headless": False})
Print Vehicle Info
def run(self):
while simulation_app.is_running():
self.world.step(render=True)
for name, vehicle in self.vehicles.items():
pos, ori = vehicle.get_world_pose()
print(f"{name}: pos={pos}, ori={ori}")
Check ROS 2 Topics
docker exec airstack-isaac-sim-1 bash -c "ros2 topic list"
docker exec airstack-isaac-sim-1 bash -c "ros2 topic hz /drone1/sensors/camera/image"
References