Create a Standalone Application

This tutorial introduces how to create a standalone python script to set up an empty scene. It introduces the main class used by Isaac Sim simulator, SimulationApp, as well as the timeline concept that helps to launch and control the simulation timeline. In this tutorial we do NOT expose the Pegasus API yet.

1. Code

The tutorial corresponds to the 0_template_app.py example in the examples directory.

  1#!/usr/bin/env python
  2"""
  3| File: 0_template_app.py
  4| Author: Marcelo Jacinto (marcelo.jacinto@tecnico.ulisboa.pt)
  5| License: BSD-3-Clause. Copyright (c) 2023, Marcelo Jacinto. All rights reserved.
  6| Description: This files serves as a template on how to build a clean and simple Isaac Sim based standalone App.
  7"""
  8
  9# Imports to start Isaac Sim from this script
 10import carb
 11from omni.isaac.kit import SimulationApp
 12
 13# Start Isaac Sim's simulation environment
 14# Note: this simulation app must be instantiated right after the SimulationApp import, otherwise the simulator will crash
 15# as this is the object that will load all the extensions and load the actual simulator.
 16simulation_app = SimulationApp({"headless": False})
 17
 18# -----------------------------------
 19# The actual script should start here
 20# -----------------------------------
 21import omni.timeline
 22from omni.isaac.core import World
 23
 24class Template:
 25    """
 26    A Template class that serves as an example on how to build a simple Isaac Sim standalone App.
 27    """
 28
 29    def __init__(self):
 30        """
 31        Method that initializes the template App and is used to setup the simulation environment.
 32        """
 33        
 34        # Acquire the timeline that will be used to start/stop the simulation
 35        self.timeline = omni.timeline.get_timeline_interface()
 36
 37        # Acquire the World, .i.e, the singleton that controls that is a one stop shop for setting up physics, 
 38        # spawning asset primitives, etc.
 39        self.world = World()
 40
 41        # Create a ground plane for the simulation
 42        self.world.scene.add_default_ground_plane()
 43
 44        # Create an example physics callback
 45        self.world.add_physics_callback('template_physics_callback', self.physics_callback)
 46
 47        # Create an example render callback
 48        self.world.add_render_callback('template_render_callback', self.render_callback)
 49
 50        # Create an example timeline callback
 51        self.world.add_timeline_callback('template_timeline_callback', self.timeline_callback)
 52
 53        # Reset the simulation environment so that all articulations (aka robots) are initialized
 54        self.world.reset()
 55
 56        # Auxiliar variable for the timeline callback example
 57        self.stop_sim = False
 58
 59    def physics_callback(self, dt: float):
 60        """An example physics callback. It will get invoked every physics step.
 61
 62        Args:
 63            dt (float): The time difference between the previous and current function call, in seconds.
 64        """
 65        carb.log_info("This is a physics callback. It is called every " + str(dt) + " seconds!")
 66
 67    def render_callback(self, data):
 68        """An example render callback. It will get invoked for every rendered frame.
 69
 70        Args:
 71            data: Rendering data.
 72        """
 73        carb.log_info("This is a render callback. It is called every frame!")
 74
 75    def timeline_callback(self, timeline_event):
 76        """An example timeline callback. It will get invoked every time a timeline event occurs. In this example,
 77        we will check if the event is for a 'simulation stop'. If so, we will attempt to close the app
 78
 79        Args:
 80            timeline_event: A timeline event
 81        """
 82        if self.world.is_stopped():
 83            self.stop_sim = True
 84
 85    def run(self):
 86        """
 87        Method that implements the application main loop, where the physics steps are executed.
 88        """
 89
 90        # Start the simulation
 91        self.timeline.play()
 92
 93        # The "infinite" loop
 94        while simulation_app.is_running() and not self.stop_sim:
 95
 96            # Update the UI of the app and perform the physics step
 97            self.world.step(render=True)
 98        
 99        # Cleanup and stop
100        carb.log_warn("Template Simulation App is closing.")
101        self.timeline.stop()
102        simulation_app.close()
103
104def main():
105
106    # Instantiate the template app
107    template_app = Template()
108
109    # Run the application loop
110    template_app.run()
111
112if __name__ == "__main__":
113    main()

2. Explanation

The first step, when creating a standalone application with Isaac Sim is to instantiate the SimulationApp object, and it takes a dictionary of parameters that can be used to configure the application. This object will be responsible for opening a bare bones version of the simulator. The headless parameter selects whether to launch the GUI window or not.

11from omni.isaac.kit import SimulationApp
12
13# Start Isaac Sim's simulation environment
14# Note: this simulation app must be instantiated right after the SimulationApp import, otherwise the simulator will crash
15# as this is the object that will load all the extensions and load the actual simulator.
16simulation_app = SimulationApp({"headless": False})

Note

You can NOT import other omniverse modules before instantiating the SimulationApp, otherwise the simulation crashes before the window it’s even open. For a deeper understanding on how the SimulationApp works, check NVIDIA’s offical documentation.

In order to create a Simulation Context, we resort to the World class environment. The World class inherits the Simulation Context, and provides already some default parameters for setting up a simulation for us. You can pass as arguments the physics time step and rendering time step (in seconds). The rendering and physics can be set to run at different rates. For now let’s use the defaults: 60Hz for both rendering and physics.

After creating the World class environment. The World class inherits the Simulation Context, we will take advantage of its callback system to declare that some functions defined by us should be called at every physics iteration, render iteration or every time there is a timeline event, such as pressing the start/stop button. In this case, the physics_callback method will be invoked at every physics step, the render_callback at every render step and timeline_callback every time there is a timeline event. You can add as many callbacks as you want. After having all your callbacks defined, the world.reset() method should be invoked to initialize the physics context and set any existing robot joints (in this case there is none) to their default state.

35        self.timeline = omni.timeline.get_timeline_interface()
36
37        # Acquire the World, .i.e, the singleton that controls that is a one stop shop for setting up physics, 
38        # spawning asset primitives, etc.
39        self.world = World()
40
41        # Create a ground plane for the simulation
42        self.world.scene.add_default_ground_plane()
43
44        # Create an example physics callback
45        self.world.add_physics_callback('template_physics_callback', self.physics_callback)
46
47        # Create an example render callback
48        self.world.add_render_callback('template_render_callback', self.render_callback)
49
50        # Create an example timeline callback
51        self.world.add_timeline_callback('template_timeline_callback', self.timeline_callback)
52
53        # Reset the simulation environment so that all articulations (aka robots) are initialized
54        self.world.reset()

To start the actual simulation, we invoke the timeline’s play() method. This is necessary in order to ensure that every previously defined callback gets invoked. In order for the Isaac Sim app to remain responsive, we need to create a while loop that invokes world.step(render=True) to make sure that the UI get’s rendered.

 85    def run(self):
 86        """
 87        Method that implements the application main loop, where the physics steps are executed.
 88        """
 89
 90        # Start the simulation
 91        self.timeline.play()
 92
 93        # The "infinite" loop
 94        while simulation_app.is_running() and not self.stop_sim:
 95
 96            # Update the UI of the app and perform the physics step
 97            self.world.step(render=True)
 98        
 99        # Cleanup and stop
100        carb.log_warn("Template Simulation App is closing.")
101        self.timeline.stop()
102        simulation_app.close()

As you may have noticed, our infinite loop is very clean. In this work, similarly to the ROS 2 standard, we prefer to perform all the logic by resorting to function callbacks instead of cramming all the logic inside the while loop. This structure allows our code to be more organized and more modular. As you will learn in the following tutorials, the Pegasus Simulator API is built on top of this idea of callbacks.

3. Execution

Now let’s run the Python script:

ISAACSIM_PYTHON examples/0_template_app.py

This should open a stage with a blue ground-plane. The simulation should start playing automatically and the stage being rendered. To stop the simulation, you can either close the window, press the STOP button in the UI, or press Ctrl+C in the terminal.

Note

Notice that the window will only close when pressing the STOP button, because we defined a method called timeline_callback that get’s invoked for every timeline event, such as pressing the STOP button.