Extension: omni.kit.scene_view.xr_utils-1.0.2

Documentation Generated: Jul 18, 2026

Examples#

Three annotated scripts live in docs/examples/. They are runnable as-is — paste one into the Kit script editor with the extension enabled, and the panel appears in the active viewport. Each script is reproduced below from its source file via literalinclude, so what you read here is exactly what ships in the extension.

1. Basic in-scene panel#

The minimum viable example: build an omni.ui.Widget, wrap it in a WidgetComponent, place it with a single SpatialSource.new_translation_source, and hand both to a UiContainer. The script walks through every constructor argument once and shows two cleanup options (drop the reference, or call hide()).

import omni.ui as ui
from omni.kit.scene_view.xr import XRSceneView
from omni.kit.scene_view.xr_utils import UiContainer, WidgetComponent, UpdatePolicy, SpatialSource
from pxr import Gf


# -- 1. Define your omni.ui widget ---------------------------------------------

class InfoPanel(ui.Widget):
    """A simple label panel to show in the XR scene."""

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        with ui.Frame():
            with ui.VStack(style={"background_color": ui.color(0.1, 0.1, 0.15, 0.9)}):
                ui.Label(
                    "Hello XR!",
                    alignment=ui.Alignment.CENTER,
                    style={"font_size": 24, "color": ui.color(1.0, 1.0, 1.0)},
                )
                ui.Label(
                    "Kit — omni.kit.scene_view.xr_utils",
                    alignment=ui.Alignment.CENTER,
                    style={"font_size": 14, "color": ui.color(0.7, 0.9, 1.0)},
                )


# -- 2. Create the WidgetComponent ---------------------------------------------
#
# Width / height are in world units (centimetres by default).
# resolution_scale * unit_to_pixel_scale controls texture quality:
#   higher → sharper but more GPU memory.

panel_component = WidgetComponent(
    widget_type=InfoPanel,
    width=100.0,              # 100 cm wide
    height=20.0,              # 20 cm tall
    resolution_scale=4.0,     # 4x supersampling on the texture
    unit_to_pixel_scale=2.0,  # 2 omni.ui pixels per stage unit (cm)
    update_policy=UpdatePolicy.ON_MOUSE_HOVERED,
)


# -- 3. Position the panel with SpatialSource ----------------------------------
#
# SpatialSource describes where in world space the UI lives.
# new_translation_source offsets by a fixed (x, y, z) vector in stage units.
# Stack multiple sources for combined transforms — they apply left-to-right.

placement = SpatialSource.new_translation_source(Gf.Vec3f(0.0, 150.0, -80.0))  # 1.5 m up, 80 cm in front


# -- 4. Wrap everything in a UiContainer ---------------------------------------
#
# UiContainer owns both the SceneView and the manipulator hierarchy.
# Dropping this object removes the UI from the scene — keep a strong reference.
# Use show() / hide() instead of recreating; recreating leaks SceneView memory.

panel = UiContainer(
    scene_view_type=XRSceneView,
    initial_component=panel_component,
    space_stack=placement,
)


# -- 5. Cleanup ----------------------------------------------------------------
#
# When your extension unloads, simply drop the reference:
#
#   panel = None
#
# Or toggle visibility without destroying the object:
#
#   panel.hide()
#   panel.show()

Try modifying:

  • Replace new_translation_source with a list [translation, look_at_camera] to make the panel always face the camera.

  • Raise resolution_scale to 8.0 to render the panel at a higher texture resolution.

  • Swap UpdatePolicy.ON_MOUSE_HOVERED for UpdatePolicy.ALWAYS to redraw every frame instead of only while hovered.

Concepts exercised: WidgetComponent, the simplest case of Spatial Sources.

2. Camera-facing HUD#

A billboard panel that always faces the viewer — the standard pattern for status readouts, FPS counters, and tooltips. The space_stack composes two sources: a translation that positions the pivot, then a look_at_camera source that rotates around that pivot every frame.

import omni.ui as ui
from omni.kit.scene_view.xr import XRSceneView
from omni.kit.scene_view.xr_utils import UiContainer, WidgetComponent, UpdatePolicy, SpatialSource
from pxr import Gf


# -- Widget --------------------------------------------------------------------

class StatusHUD(ui.Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        with ui.Frame():
            with ui.VStack(
                style={
                    "background_color": ui.color(0.05, 0.05, 0.1, 0.85),
                    "border_radius": 8,
                }
            ):
                ui.Label(
                    "Status: Connected",
                    alignment=ui.Alignment.CENTER,
                    style={"font_size": 18, "color": ui.color(0.2, 1.0, 0.4)},
                )
                ui.Spacer(height=4)
                ui.Label(
                    "FPS: 90",
                    alignment=ui.Alignment.CENTER,
                    style={"font_size": 14, "color": ui.color(0.8, 0.8, 0.8)},
                )


# -- Component -----------------------------------------------------------------

hud_component = WidgetComponent(
    widget_type=StatusHUD,
    width=80.0,
    height=20.0,
    resolution_scale=5.0,
    unit_to_pixel_scale=2.0,
    update_policy=UpdatePolicy.ALWAYS,  # update every frame so status stays fresh
)


# -- Spatial stack -------------------------------------------------------------
#
# The sources are applied in list order.
# Translating first places the pivot, then LookAtCamera rotates around it.

space_stack = [
    SpatialSource.new_translation_source(Gf.Vec3f(0.0, 170.0, -60.0)),
    SpatialSource.new_look_at_camera_source(),
]


# -- Container -----------------------------------------------------------------

status_hud = UiContainer(
    scene_view_type=XRSceneView,
    initial_component=hud_component,
    space_stack=space_stack,
)

Try modifying:

  • Reorder the space_stack to put look_at_camera first. The result is visually identical here, because the look-at source contributes rotation only and there is nothing before the translation to rotate around — see Example 3 for a case where the order does matter.

  • Drop UpdatePolicy.ALWAYS to ON_MOUSE_HOVERED. The HUD will stop refreshing until you point at it.

Concepts exercised: composing a two-source space_stack, billboard behaviour via SpatialSource.new_look_at_camera_source.

3. Prim-anchored label#

Attach a label to a USD prim’s world transform via SpatialSource.new_prim_path_source, then lift the label 30 cm above the prim’s origin and have it face the viewer. This example shows the most subtle composition rule: the prim-path source overrides preceding contributions, so it must come first in the stack, with relative offsets afterwards.

import omni.ui as ui
from omni.kit.scene_view.xr import XRSceneView
from omni.kit.scene_view.xr_utils import UiContainer, WidgetComponent, UpdatePolicy, SpatialSource
from pxr import Gf


ASSET_PRIM_PATH = "/World/MyMachine"  # the prim this UI should follow


# -- Widget --------------------------------------------------------------------

class ObjectLabel(ui.Widget):
    def __init__(self, prim_path: str, **kwargs):
        super().__init__(**kwargs)
        name = prim_path.split("/")[-1]
        with ui.Frame():
            with ui.HStack(
                style={
                    "background_color": ui.color(0.1, 0.1, 0.2, 0.88),
                    "padding": 6,
                    "border_radius": 4,
                }
            ):
                ui.Label(
                    f"+  {name}",
                    alignment=ui.Alignment.CENTER,
                    style={"font_size": 20, "color": ui.color(1.0, 0.85, 0.4)},
                )


# -- Component -----------------------------------------------------------------

label_component = WidgetComponent(
    widget_type=ObjectLabel,
    width=75.0,
    height=10.0,
    resolution_scale=4.0,
    unit_to_pixel_scale=2.0,
    update_policy=UpdatePolicy.ON_MOUSE_HOVERED,
    widget_kwargs={"prim_path": ASSET_PRIM_PATH},
)


# -- Spatial stack -------------------------------------------------------------
#
# PrimPathSpace anchors the pivot to the prim's world transform.
# The translation afterwards lifts the label above the object's origin.

space_stack = [
    SpatialSource.new_prim_path_source(ASSET_PRIM_PATH),
    SpatialSource.new_translation_source(Gf.Vec3f(0.0, 30.0, 0.0)),  # 30 cm above prim origin
    SpatialSource.new_look_at_camera_source(),                         # always face the viewer
]


# -- Container -----------------------------------------------------------------

object_label = UiContainer(
    scene_view_type=XRSceneView,
    initial_component=label_component,
    space_stack=space_stack,
)

# To update the label text dynamically (e.g. on a stage event), call:
#
#   label_component.scene_widget.invalidate()
#
# This triggers a rebuild of the widget's omni.ui tree.

Try modifying:

  • Change ASSET_PRIM_PATH to point at a prim in your stage. The label picks up the new prim on the next frame.

  • Swap the order of the prim-path and translation sources. The label will end up at the world origin offset, ignoring the prim — see the override semantics described in Spatial Sources.

  • Reach into label_component.widget to drive the label text from a stage event, then call label_component.scene_widget.invalidate() to apply the change.

Concepts exercised: prim anchoring, three-source composition, the widget_kwargs pattern for passing constructor data into a wrapped widget.

See also#

  • Spatial Sources — the composition rules used in examples 2 and 3.

  • WidgetComponent — the sizing and update-policy concepts behind every example’s component.

  • Migration — if you have existing code using the deprecated SceneWidgetManipulator, this is where the example patterns slot in.