How to make an Object Info Widget Extension
This guide will provide you with a starting point for displaying Object Info and nesting these modules into a Widget. A Widget is a useful utility in Omniverse Kit that can be used to add features such as buttons and sliders.
Learning Objectives
In this guide you will learn how to:
Create a Widget Extension
Use Omniverse UI Framework
Create a label
(optional) Create a toggle button feature
(optional) Create a slider
Prerequisites
It is recommended that you have completed the following:
Step 1: Create a Widget Module
In this series of steps, you will be setting up your Extension to create a module needed for a widget.
Step 1.1: Clone Slider Tutorial Branch
Clone the slider-tutorial-start
branch of the kit-extension-sample-ui-scene github repository to get the assets needed for this hands-on lab.
Step 1.2: Add Extension Path to Extension Manager
Open the Extensions Manager in Omniverse Code.
Select gear icon to display Extension Search Paths.
Use the green ➕ symbol to add the path to
exts/slider-tutorial-start
from the cloned directory.
Note
Check that the UI Scene Object Info Extension is enabled in the Extensions Manager and working by creating a new primitive in the Viewport and selecting it, the object’s path and info should be displayed above the object.
Step 1.3 Open VS Code with Shortcut
Open VS Code directly from the Extension Manager
Note
If you would like to know more about how to create the modules for displaying Object Info, check out the guide here.
Step 1.4: Create the Module
Create a new module called object_info_widget.py
in the exts
hierarchy that our other modules are located in. This will be our widget module.
object_info_widget.py
will be building off the Object Info modules provided for you. You will see these modules as object_info_manipulator.py
, object_info_model.py
, viewport_scene.py
, and an updated extension.py
.
Note
Visual Studio Code (VS Code) is our preferred IDE, hence forth referred to throughout this guide.
Step 1.5: Set up Widget Class
Inside of the object_info_widget.py
, import omni.ui
then create
the WidgetInfoManipulator
class to nest our functions. After,
initialize our methods, as so:
from omni.ui import scene as sc
from omni.ui import color as cl
import omni.ui as ui
class WidgetInfoManipulator(sc.Manipulator):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.destroy()
def destroy(self):
self._root = None
self._name_label = None
This widget will house our widget info to make the information contrasted in the viewport and add other utilities later on.
You will accomplish this by creating a box for the label with a background color.
Step 1.6: Build the widget
Let’s define this as on_build_widgets
and use the Omniverse UI Framework
to create the label for this widget in a ZStack
. See here for more documentation on Omniverse UI Framework.
...
def on_build_widgets(self):
with ui.ZStack():
Once you have established the UI layout, you can create the background for the widget using ui.Rectangle
and set the border attributes and background color. You can then create the ui.Label
and set its alignment, as so:
...
def on_build_widgets(self):
with ui.ZStack():
ui.Rectangle(style={
"background_color": cl(0.2),
"border_color": cl(0.7),
"border_width": 2,
"border_radius": 4,
})
self._name_label = ui.Label("", height=0, alignment=ui.Alignment.CENTER)
Step 1.7: Create Manipulator Functions
With a Manipulator, you need to define an on_build
function.
This function is called when the model is changed so that the widget is rebuilt. You can find more information about the Manipulator here.
...
self.on_model_updated(None)
def on_build(self):
"""Called when the model is changed and rebuilds the whole slider"""
self._root = sc.Transform(visible=False)
with self._root:
with sc.Transform(scale_to=sc.Space.SCREEN):
with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, 100, 0)):
self._widget = sc.Widget(500, 150, update_policy=sc.Widget.UpdatePolicy.ON_MOUSE_HOVERED)
self._widget.frame.set_build_fn(self.on_build_widgets)
Now define on_model_updated()
that was called above. In this function you need to establish what happens if nothing is selected, when to update the prims, and when to update the prim name, as so:
...
def on_model_updated(self, _):
# if you don't have selection then show nothing
if not self.model or not self.model.get_item("name"):
self._root.visible = False
return
# Update the shapes
position = self.model.get_as_floats(self.model.get_item("position"))
if self._root:
self._root.transform = sc.Matrix44.get_translation_matrix(*position)
self._root.visible = True
# Update the shape name
if self._name_label:
self._name_label.text = f"Prim:{self.model.get_item('name')}"
Click here for the end code of widget_info_manipulator.py
from omni.ui import scene as sc
from omni.ui import color as cl
import omni.ui as ui
class WidgetInfoManipulator(sc.Manipulator):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.destroy()
def destroy(self):
self._root = None
self._name_label = None
def on_build_widgets(self):
with ui.ZStack():
ui.Rectangle(style={
"background_color": cl(0.2),
"border_color": cl(0.7),
"border_width": 2,
"border_radius": 4,
})
self._name_label = ui.Label("", height=0, alignment=ui.Alignment.CENTER)
self.on_model_updated(None)
def on_build(self):
"""Called when the model is changed and rebuilds the whole slider"""
self._root = sc.Transform(visible=False)
with self._root:
with sc.Transform(scale_to=sc.Space.SCREEN):
with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, 100, 0)):
self._widget = sc.Widget(500, 150, update_policy=sc.Widget.UpdatePolicy.ON_MOUSE_HOVERED)
self._widget.frame.set_build_fn(self.on_build_widgets)
def on_model_updated(self, _):
# if you don't have selection then show nothing
if not self.model or not self.model.get_item("name"):
self._root.visible = False
return
# Update the shapes
position = self.model.get_as_floats(self.model.get_item("position"))
if self._root:
self._root.transform = sc.Matrix44.get_translation_matrix(*position)
self._root.visible = True
# Update the shape name
if self._name_label:
self._name_label.text = f"Prim:{self.model.get_item('name')}"
Step 2: Update Viewport and Extension
Now that you have created a new module, it is important for us to bring this information into viewport_scene.py
and update extension.py
to reflect these new changes.
Step 2.1: Import Widget Info
Begin by updating viewport_scene.py
and importing WidgetInfoManipulator
at the top of the file with the other imports.
from omni.ui import scene as sc
import omni.ui as ui
from .object_info_manipulator import ObjInfoManipulator
from .object_info_model import ObjInfoModel
# NEW
from .widget_info_manipulator import WidgetInfoManipulator
# END NEW
Step 2.2: Add Display Widget
Inside the ViewportSceneInfo
class, you will add a display_widget
parameter to __init__()
:
...
class ViewportSceneInfo():
# NEW PARAMETER: display_widget
def __init__(self, viewport_window, ext_id, display_widget) -> None:
self.scene_view = None
self.viewport_window = viewport_window
...
Step 2.3: Use display_widget
Use display_widget
to control whether to show WidgetInfoManipulator
or ObjInfoManipulator
as so:
...
class ViewportSceneInfo():
# NEW PARAMETER: display_widget
def __init__(self, viewport_window, ext_id, display_widget) -> None:
self.scene_view = None
self.viewport_window = viewport_window
with self.viewport_window.get_frame(ext_id):
self.scene_view = sc.SceneView()
with self.scene_view.scene:
# NEW
if display_widget:
WidgetInfoManipulator(model=ObjInfoModel())
else:
# END NEW
ObjInfoManipulator(model=ObjInfoModel())
...
Click here for the updated viewport_scene.py
from omni.ui import scene as sc
import omni.ui as ui
from .object_info_manipulator import ObjInfoManipulator
from .object_info_model import ObjInfoModel
from .widget_info_manipulator import WidgetInfoManipulator
class ViewportSceneInfo():
def __init__(self, viewport_window, ext_id, display_widget) -> None:
self.scene_view = None
self.viewport_window = viewport_window
with self.viewport_window.get_frame(ext_id):
self.scene_view = sc.SceneView()
with self.scene_view.scene:
if display_widget:
WidgetInfoManipulator(model=ObjInfoModel())
else:
ObjInfoManipulator(model=ObjInfoModel())
self.viewport_window.viewport_api.add_scene_view(self.scene_view)
def __del__(self):
self.destroy()
def destroy(self):
if self.scene_view:
self.scene_view.scene.clear()
if self.viewport_window:
self.viewport_window.viewport_api.remove_scene_view(self.scene_view)
self.viewport_window = None
self.scene_view = None
Step 3: Update extension.py
Now that you have created the widget and passed it into the viewport, you need to call this in the extension.py
module for it to function.
Step 3.1: Edit the Class Name
Start by changing the class name of extension.py
from MyExtension
to something more descriptive, like ObjectInfoWidget
:
...
## Replace ##
class MyExtension(omni.ext.IExt):
## With ##
class ObjectInfoWidget(omni.ext.IExt):
## END ##
def __init__(self) -> None:
super().__init__()
self.viewportScene = None
Step 3.2: Pass the Parameter
Pass the new parameter in on_startup()
as follows:
...
def on_startup(self, ext_id):
#Grab a reference to the viewport
viewport_window = get_active_viewport_window()
# NEW PARAMETER PASSED
self.viewportScene = ViewportSceneInfo(viewport_window, ext_id, True)
...
Click here for the updated extension.py module
import omni.ext
import omni.ui as ui
from omni.ui import scene as sc
from omni.ui import color as cl
from omni.kit.viewport.utility import get_active_viewport_window
from .viewport_scene import ViewportSceneInfo
class ObjectInfoWidget(omni.ext.IExt):
def __init__(self) -> None:
super().__init__()
self.viewportScene = None
def on_startup(self, ext_id):
#Grab a reference to the viewport
viewport_window = get_active_viewport_window()
self.viewportScene = ViewportSceneInfo(viewport_window, ext_id, True)
def on_shutdown(self):
if self.viewportScene:
self.viewportScene.destroy()
self.viewportScene = None
Excellent, You should now see these updates in Omniverse Code at this point.
Step 5: Add a Slider
In this step, you will be adding a slider to the widget. This slider will change the scale of the object. This is an optional step and may be skipped as it is just to showcase a simple addition of what a widget can do. For a more complex slider, check out the guide to `Slider Manipulator here. <https://github.com/NVIDIA-Omniverse/kit-extension-sample-ui-scene/blob/main/exts/omni.example.ui_scene.slider_manipulator/Tutorial/slider_Manipulator_Tutorial.md>`__
Step 5.1: Add to widget_info_manipulator.py
Use omni.ui
to build the framework for the slider in the function on_build_widgets()
.
This slider is an optional feature to the widget but is a great way to add utility.
...
def on_build_widgets(self):
with ui.ZStack():
ui.Rectangle(style={
"background_color": cl(0.2),
"border_color": cl(0.7),
"border_width": 2,
"border_radius": 4,
})
# NEW: Adding the Slider to the widget in the scene
with ui.VStack(style={"font_size": 24}):
self._name_label = ui.Label("", height=0, alignment=ui.Alignment.CENTER)
# setup some model, just for simple demonstration here
self._slider_model = ui.SimpleFloatModel()
ui.Spacer(height=5)
with ui.HStack():
ui.Spacer(width=10)
ui.Label("scale", height=0, width=0)
ui.Spacer(width=5)
ui.FloatSlider(self._slider_model)
ui.Spacer(width=5)
ui.Spacer(height=5)
# END NEW
self.on_model_updated(None)
...
Step 5.2: Update the Scale with a Slider Function
Add a new function that will scale the model when the slider is dragged.
Define this function after on_model_updated
and name it update_scale
.
...
def on_model_updated(self, _):
# if you don't have selection then show nothing
if not self.model or not self.model.get_item("name"):
self._root.visible = False
return
# Update the shapes
position = self.model.get_as_floats(self.model.get_item("position"))
if self._root:
self._root.transform = sc.Matrix44.get_translation_matrix(*position)
self._root.visible = True
# NEW
# Update the slider
def update_scale(prim_name, value):
print(f"changing scale of {prim_name}, {value}")
stage = self.model.usd_context.get_stage()
prim = stage.GetPrimAtPath(self.model.current_path)
scale = prim.GetAttribute("xformOp:scale")
scale.Set(Gf.Vec3d(value, value, value))
if self._slider_model:
self._slider_subscription = None
self._slider_model.as_float = 1.0
self._slider_subscription = self._slider_model.subscribe_value_changed_fn(
lambda m, p=self.model.get_item("name"): update_scale(p, m.as_float)
)
# END NEW
...
Click here for the updated widget_info_manipulator.py module
from omni.ui import scene as sc
from omni.ui import color as cl
import omni.ui as ui
from pxr import Gf
class WidgetInfoManipulator(sc.Manipulator):
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self.destroy()
def destroy(self):
self._root = None
self._name_label = None
self._slider_model = None
def on_build_widgets(self):
with ui.ZStack():
ui.Rectangle(style={
"background_color": cl(0.2),
"border_color": cl(0.7),
"border_width": 2,
"border_radius": 4,
})
with ui.VStack():
self._name_label = ui.Label("", height=0, alignment=ui.Alignment.CENTER)
# setup some model, just for simple demonstration here
self._slider_model = ui.SimpleFloatModel()
ui.Spacer(height=5)
with ui.HStack():
ui.Spacer(width=10)
ui.Label("scale", height=0, width=0)
ui.Spacer(width=5)
ui.FloatSlider(self._slider_model)
ui.Spacer(width=5)
ui.Spacer(height=5)
self.on_model_updated(None)
def on_build(self):
"""Called when the model is changed and rebuilds the whole slider"""
self._root = sc.Transform(visible=False)
with self._root:
with sc.Transform(scale_to=sc.Space.SCREEN):
with sc.Transform(transform=sc.Matrix44.get_translation_matrix(0, 100, 0)):
self._widget = sc.Widget(500, 150, update_policy=sc.Widget.UpdatePolicy.ON_MOUSE_HOVERED)
self._widget.frame.set_build_fn(self.on_build_widgets)
def on_model_updated(self, _):
# if you don't have selection then show nothing
if not self.model or not self.model.get_item("name"):
self._root.visible = False
return
# Update the shapes
position = self.model.get_as_floats(self.model.get_item("position"))
if self._root:
self._root.transform = sc.Matrix44.get_translation_matrix(*position)
self._root.visible = True
# Update the slider
def update_scale(prim_name, value):
print(f"changing scale of {prim_name}, {value}")
stage = self.model.usd_context.get_stage()
prim = stage.GetPrimAtPath(self.model.current_path)
scale = prim.GetAttribute("xformOp:scale")
scale.Set(Gf.Vec3d(value, value, value))
if self._slider_model:
self._slider_subscription = None
self._slider_model.as_float = 1.0
self._slider_subscription = self._slider_model.subscribe_value_changed_fn(
lambda m, p=self.model.get_item("name"): update_scale(p, m.as_float)
)
# Update the shape name
if self._name_label:
self._name_label.text = f"Prim:{self.model.get_item('name')}"
Here is what is created in the viewport of Omniverse Code:
Congratulations!
You have successfully created a Widget Info Extension!