Skip to main content
Version: 21 Aug 2024

Using Kit In-Scene UI with MagicLeap 2

This document describes how to use the AR Experience plugin with the Magic Leap 2 headset and Omniverse applications. In this tutorial, you will learn how to build kit applications with UX that's designed for use in XR.

Prerequisites

Omniverse | MagicLeap Developer Documentation - this documentation shows you how to set up USD Composer for use with a Magic Leap 2 (ML2) headset and enable the AR Experience plugin.

caution

This version of the AR Experience plugin is an alpha release. The AR Experience plugin for kit is currently in alpha. Not all ML2 platform features are available and the content of this article may change in the future.

Interacting with omni.UI Widgets

Any UI that is part of the Omniverse UI framework can be rendered and interacted with in 3D as a part of in-scene UI. The omni.kit.xr.example.sceneview_utils plugin provides a set of example UI widgets that can be interacted with using the ML2 controller.

Enable Example Extensions

To enable the example extensions:

  1. Navigate to Window > Extensions.
  2. Search for usd-based.
  3. Install and enable the USD-Based SceneUI Example (omni.kit.xr.example.usd_scene_ui) extension.
USD-Based Scene UI

This extension contains a set of examples that show how to use UI Widgets as in-scene UI using the UsdSceneView class provided by the omni.kit.xr.sceneview_utils helper module.

USD-Based Scene UI
  1. After enabling the extension a new Menu Item will appear called Examples.
Examples Menu Item
Make sure your stage is Y-Up. Currently, the UI widgets are placed based on Y-Up stages.

Set Current Stage Axis

To set the current stage axis:

  1. Go to the Layer Tab.

  2. Select Root Layer.

  3. Go to the Properties Tab.

  4. Under Layer Metadata, locate World Axis and select the drop-down to change it to Y.

World Axis Under Layer Metadata

Set Default Stage Axis

To set the default stage axis so that every new stage will have the Up axis based on the default stage axis, do the following:

  1. Go to the Menu item Edit > Preferences.

  2. In the Left tab find and select Stage.

  3. For Default Up Axis, select Y in the drop-down menu.

Set Default Up Axis
  1. From the Examples menu, select USD Scene UI Control to see several examples of simple UI widgets.
UI Widget Examples

You can use a combination of UI widgets and spatial input to enable entirely XR based workflows in your kit applications, like USD Composer.

Extensions for Creating Prims

The Scene UI Examples include useful extensions out of the box that you can use to create basic prims in your scene and manipulate using the same TransformAttributeWidget that you would see in the Property panel in USD Composer.

Prim Maker

Position refers to the coordinate position for where the prim will appear. Selecting the button for a prim spawns it. For example, pressing the Cube button spawns a Cube prim.

Prim Transform

By selecting a prim in the scene a UI that looks similar to the Transform Property in the Property tab appears above the prim. Using the pointer, you can click and drag these values to update and change the prim's transform properties.

Building a New Kit Extension with an Interactive Widget

This section shows an example of how to create a new kit extension with an interactive widget.

New Kit Extension
tip

We recommend that you install Visual Studio Code and the official Python extension from Microsoft for the best Kit plugin development experience with Omniverse.

You can use the extension template included in Omniverse to bootstrap a new project. To do that, take these steps:

  1. In USD Composer, from the Window menu, select Extensions to open the extension browser.

  2. In the extension browser window, click on the plus button and choose New Extension Template Project.

New Extension Template Project
  1. Select a location to save the extension project files and give the extension a name. (See Omniverse Kit documentation for more information.)

  2. The template generates a Python source file with an extension class that implements a simple UI window that looks like this:

Simple UI Window
  1. You can modify the sample UI code for the window to render the same controls as in-scene UI that can be interacted with using the ML2 controller. To do this you can use utility classes that are included with thh omni.kit.xr.sceneview_utils plugin out of the box. We will modify the template code to build the UI layout inside of a generic UI Widget that can be rendered inside of a container in the USD scene, instead of a 2D window frame. The original UI layout code generated by the template looks like this:
class MagicleapInSceneUITutorialExtension(omni.ext.IExt):
def on_startup(self, ext_id):
print("[magicleap.insceneuitutorial] magicleap in scene ui tutorial extension startup")

self._count = 0

self._window = ui.Window("My Window", width=300, height=300)
with self._window.frame:
with ui.VStack():
label = ui.Label("")

def on_click():
self._count += 1
label.text = f"count: {self._count}"

def on_reset():
self._count = 0
label.text = "empty"

on_reset()

with ui.HStack():
ui.Button("Add", clicked_fn=on_click)
ui.Button("Reset", clicked_fn=on_reset)
# ...
  1. Take the control layout from the original code, and lift it into a separate class that extends ui.Widget as shown:
class MagicleapWidget(ui.Widget):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._count = 0
self._build_ui()

def __del__(self):
self.destroy()

def _build_ui(self):
self._widget = ui.VStack()
with self._widget:
label = ui.Label("")

def on_click():
self._count += 1
label.text = f"count: {self._count}"

def on_reset():
self._count = 0
label.text = "empty"

on_reset()

with ui.HStack():
ui.Button("Add", clicked_fn=on_click)
ui.Button("Reset", clicked_fn=on_reset)
  1. The SceneViewUtils helpers make it easy to render the same UI controls inside a 3D container that can be rendered and interacted with along with other stage content. Add the following imports to your code to access scene view utility classes:
from carb import Float2, Float3
from omni.ui import scene as sc

from omni.kit.scene_view.usd import UsdSceneView
from omni.kit.xr.sceneview_utils import SceneViewUtils, SceneWidgetManipulator
  1. Use the _scene_view_utils.create_widget_factory function to create an instance of the widget inside of a container in the scene:
class MagicleapInSceneUITutorialExtension(omni.ext.IExt):
def __init__(self) -> None:
super().__init__()
self._scene_view_utils: SceneViewUtils | None = None
self._widget_container: SceneWidgetManipulator[MagicleapWidget] | None = None

def on_startup(self, ext_id):
print("[magicleap.insceneuitutorial] magicleap in scene ui tutorial extension startup")

if self._scene_view_utils is None:
self._scene_view_utils = SceneViewUtils(UsdSceneView)

self._widget_container = (
self._scene_view_utils.create_widget_factory(MagicleapWidget)
.with_size(Float2(600, 300))
.with_position(Float3(0, 300, 0))
.with_resolution_scale(3)
.with_construct_args()
.with_update_policy(sc.Widget.UpdatePolicy.ALWAYS)
.build()
)

def on_shutdown(self):
print("[magicleap.insceneuitutorial] magicleap in scene ui tutorial extension shutdown")

if self._widget_container:
self._widget_container.clear()
self._widget_container = None

You should now see the same UI rendered in the scene. The ML2 controller can be used to select the buttons.

In scene UI rendering

Now that we have a functional in-scene UI, we can experiment with binding functionality to the buttons to manipulate the scene.

Here are two sample functions that you can copy into your extension project to spawn and remove spheres from the scene:

from pxr import Sdf
from omni.timeline import get_timeline_interface

# Spawn a sphere prim in the scene
def spawn_sphere():
# Ensure stage
stage = omni.usd.get_context().get_stage()
if stage is None:
return

# Ensure that the timeline is set to play to see physics act on the sphere.
timeline = get_timeline_interface()
if timeline.is_stopped():
timeline.play()

# Generate a path for the sphere
sphere_path = Sdf.Path(omni.usd.get_stage_next_free_path(stage, "/World/Sphere", False))

# Create a sphere prim with a default transform
omni.kit.commands.execute('CreateMeshPrimWithDefaultXform',
prim_type='Sphere',
prim_path=sphere_path,
select_new_prim=True,
prepend_default_prim=True,
above_ground=True)

# Pose the sphere prim above the buttons
omni.kit.commands.execute('TransformMultiPrimsSRTCpp',
count=1,
paths=[str(sphere_path)],
new_translations=[0.0, 350.0, 0.0],
new_scales=[1.0, 1.0, 1.0])

# Attach a rigid body to the sphere and watch it fall!
omni.kit.commands.execute('SetRigidBody',
path=sphere_path,
approximationShape='convexHull',
kinematic=False)

# Delete all spheres from the scene
def delete_spheres():
# Ensure stage
stage = omni.usd.get_context().get_stage()
if stage is None:
return

# Find all spheres in the scene
spheres = []
for prim in stage.Traverse():
name = str(prim.GetName())
if "Sphere" in name:
spheres.append(prim.GetPath())

# Delete all of the spheres that were found
omni.kit.commands.execute('DeletePrims',
paths=spheres,
destructive=False)

With those two functions added, we can slightly modify the UI widget to bind them to two buttons:

# ui.Button("Add", clicked_fn=on_click)
# ui.Button("Reset", clicked_fn=on_reset)
ui.Button("Spawn", clicked_fn=spawn_sphere)
ui.Button("Delete", clicked_fn=delete_spheres)

Pressing the buttons with the ML2 controller will now add and remove spheres from the scene.

In scene UI rendering with falling spheres

Note that, as implemented, attempting to create the UI widget will fail if the plugin is enabled before the stage has opened. This will most likely happen if the extension is set to autoload when the application starts.

We can refactor the code a bit to make the extension robust to this scenario by handling stage events that are raised when the current stage state changes. We want to defer creating the UI until the stage enters the "OPENED" state and destroy it when the stage transitions out of it.

Refactored extension code-

class MagicleapInSceneUITutorialExtension(omni.ext.IExt):
def __init__(self) -> None:
# ...

# Called when this extension is loaded
def on_startup(self, ext_id):
print("[magicleap.insceneuitutorial] magicleap in scene ui tutorial extension startup")

self._stage_evt_handler = omni.usd.get_context().get_stage_event_stream() \
.create_subscription_to_pop(self.on_stage_event, name="handlestageevents")

if (omni.usd.get_context().get_stage_state() == omni.usd.StageState.OPENED):
self._create_widget()

# Called when this extension is unloaded
def on_shutdown(self):
print("[magicleap.insceneuitutorial] magicleap in scene ui tutorial extension shutdown")

if self._stage_evt_handler:
self._stage_evt_handler.unsubscribe()
self._stage_evt_handler = None

self._destroy_widget()

# The UI widget can only be instantiated in the current usd stage after one has been opened.
def on_stage_event(self, e: events.IEvent):
if e.type == int(omni.usd.StageEventType.OPENED):
self._create_widget()
elif e.type == int(omni.usd.StageEventType.CLOSING):
self._destroy_widget()

# Leverage SceneViewUtils to create UI elements in the current usd stage
def _create_widget(self):
if self._scene_view_utils is None:
self._scene_view_utils = SceneViewUtils(UsdSceneView)

if self._widget_container is None:
self._widget_container = \
self._scene_view_utils.create_widget_factory(MagicleapWidget) \
.with_size(Float2(600, 300)) \
.with_position(Float3(0, 300, 0)) \
.with_resolution_scale(3) \
.with_construct_args() \
.with_update_policy(sc.Widget.UpdatePolicy.ALWAYS) \
.build()

# Remove UI elements from the stage
def _destroy_widget(self):
if self._widget_container:
self._widget_container.clear()
self._widget_container = None

Sample source code is available on GitHub:

https://github.com/magicleap/MagicLeap-Omniverse-In-Scene-UI-Tutorial