[Omniverse] 「Omni.UI Scene」でViewportにオーバレイ その2

  • by

2022/12/21 : Omniverse Create 2022.3.1(Kit.104.1)で動作するように追記。

前回の続きで、Omni.UI Sceneを使ったViewportのオーバレイ描画を行います。
今回は特定の形状にPrim名を表示する、という例です。
Omniverse Create 2022.1.4/Omniverse Code 2022.1.2で確認しました。
Omniverse Create 2022.3.1(Kit.104.1)で確認しました。

以下にExtension"ft_lab.sample.uiSceneShowPrimName"を置いています。
https://github.com/ft-lab/omniverse_sample_scripts/tree/main/Extensions/ft_lab.sample.uiSceneShowPrimName

複数のビューポートの切り替えにも対応しています。

"ft_lab.sample.uiSceneShowPrimName"を実行

"ft_lab.sample.uiSceneShowPrimName"をOmniverse CreateまたはOmniverse Codeの"exts"フォルダに格納し、このExtensionをOnにします。
Primを選択すると、Primのローカル座標での原点位置にPrim名が表示されます。

コードの説明

"ft_lab.sample.uiSceneShowPrimName"の"ft_lab/sample/uiSceneShowPrimName/extension.py"が対象の実装になります。

omni.ui.sceneのSceneViewを使ってViewportにオーバレイして描画しています。
また、カメラの移動を「Tf.Notice.Register」を使って「Usd.Notice.ObjectsChanged」を通知しています。
これは前回の「[Omniverse] 「Omni.UI Scene」でViewportにオーバレイ」と同じ実装です。

描画自身は「SceneDraw」というクラスに渡し、
再描画(invalidate)を

  • Usd.Notice.ObjectsChangedの通知が呼ばれた場合
  • ビューポートのカメラが変更された場合(カメラパラメータの変更、カメラの切り替え)
  • updateイベントを別途登録して0.2秒ごとに選択状態が変化した場合

のタイミングで呼ぶようにしました。

ヘッダ部

from pxr import Usd, UsdGeom, CameraUtil, UsdShade, UsdSkel, Sdf, Gf, Tf
import omni.ext
import omni.ui
from omni.ui import color as cl
from omni.ui import scene as sc
import omni.kit
import omni.kit.app
import carb.events
from pathlib import Path
import time

# Kit104 : changed from omni.kit.viewport_legacy to omni.kit.viewport.utility.get_active_viewport_window
import omni.kit.viewport.utility

選択Primのワールド座標位置を計算するために"UsdGeom.XformCache"や"UsdSkel.DecomposeTransform"を呼んでいます。
そのため、from pxrのところでUsdSkelを追加しています。

updateイベントとして「omni.kit.app.get_app().get_update_event_stream()」「carb.events.IEvent」を使うため、omni.kit.appやcarb.eventsをimportしてます。
0.2秒間隔を計るために、timeをimportしてます。

Kit.103から追加された「omni.kit.viewport.utility」にアクセスするため、omni.kit.viewport.utilityをimportしてます。

描画処理部

omni.ui.sceneへの描画はクラスに分けています。

class SceneDraw (sc.Manipulator):
    _viewport_api = None

    def __init__(self, viewport_api, **kwargs):
        super().__init__ (**kwargs)

        # Set Viewport API.
        self._viewport_api = viewport_api

    def on_build (self):
        stage = omni.usd.get_context().get_stage()

        # Get selection.
        selection = omni.usd.get_context().get_selection()
        paths = selection.get_selected_prim_paths()

        time_code = Usd.TimeCode.Default()
        xformCache = UsdGeom.XformCache(time_code)

        for path in paths:
            prim = stage.GetPrimAtPath(path)
            if prim.IsValid():
                # Get world Transform.
                globalPose = xformCache.GetLocalToWorldTransform(prim)

                # Decompose transform.
                translate, rotation, scale = UsdSkel.DecomposeTransform(globalPose)

                # World to NDC space (X : -1.0 to +1.0, Y : -1.0 to +1.0).
                ndc_pos = self._viewport_api.world_to_ndc.Transform(translate)

                # Translation matrix.
                moveT = sc.Matrix44.get_translation_matrix(ndc_pos[0], ndc_pos[1], 0.0)

                with sc.Transform(transform=moveT):
                    sc.Label(prim.GetName(), alignment = omni.ui.Alignment.CENTER, color=cl("#ffff00a0"), size=20)

        #self.invalidate()

SceneViewの第一引数にViewport APIの参照を渡しています。
Viewport APIにより、アクティブなビューポートの情報を取得します。

「on_build」内に1回分の描画処理を記載します。
ここでは選択されたPrimを取得し、それぞれのワールド座標位置を求めています。
以下で4x4行列が取得されます。

globalPose = xformCache.GetLocalToWorldTransform(prim)

これを移動/回転/スケールごとに分解します。

translate, rotation, scale = UsdSkel.DecomposeTransform(globalPose)

ワールド座標位置(translate)をスクリーン上の「NDC座標」に変換します。
これは、ビューポートの横方向(X) -1.0~+1.0、縦方向(Y) -1.0~+1.0とした座標系です。

Viewport APIのメソッドを使用して"self._viewport_api.world_to_ndc.Transform"でNDC座標に変換します。
さらに"sc.Matrix44.get_translation_matrix"でNDC座標のXY位置を行列に変換しています。
"with sc.Transform(transform=moveT)"で中心位置を変更しています。
最後にラベル"sc.Label"としてPrim名を描画してます。

# World to NDC space (X : -1.0 to +1.0, Y : -1.0 to +1.0).
ndc_pos = self._viewport_api.world_to_ndc.Transform(translate)

# Translation matrix.
moveT = sc.Matrix44.get_translation_matrix(ndc_pos[0], ndc_pos[1], 0.0)

with sc.Transform(transform=moveT):
    sc.Label(prim.GetName(), alignment = omni.ui.Alignment.CENTER, color=cl("#ffff00a0"), size=20)

最後の「self.invalidate()」を有効にすると、常に選択されているPrimパス名の取得と描画が呼ばれることになります。
これは負荷になりうりますので、再描画を促すタイミングは外部から与えるようにしました。

カメラ変更/Prim変更イベント

このサンプルでは「NDC座標」でビューポートのオーバレイを扱うようにしています。
Viewport APIを使ってワールド座標からNDC座標に変換しているため、カメラのViewやProjection変換行列は渡していません。

イベントの登録

self._objects_changed_listener = Tf.Notice.Register(
    Usd.Notice.ObjectsChanged, self._notice_objects_changed, self._stage)

として何かPrimの変更(マニピュレータによる移動など)があった場合に"self._notice_objects_changed"がコールバックされます。

これに加えて、ビューポートのカメラが切り替えられたりカメラパラメータが変更された場合のイベントを登録するようにしました。

# Register a callback to be called when the camera in the viewport is changed.
self._subs_viewport_change = self._viewport_api.subscribe_to_view_change(self._viewport_changed)

Viewport APIの"subscribe_to_view_change"を使用して登録します。

イベントの破棄

イベントの破棄は以下のように記述しました。

if self._objects_changed_listener:
    self._objects_changed_listener.Revoke()

self._objects_changed_listener = None
self._subs_viewport_change = None

イベントでの更新処理

# ------------------------------------------------.
# Notification of object changes.
# ------------------------------------------------.
def _notice_objects_changed (self, notice, stage):
    # Update drawing.
    if self._sceneDraw != None:
        self._sceneDraw.invalidate()

# ------------------------------------------------.
# Called when the camera in the viewport is changed.
# ------------------------------------------------.
def _viewport_changed (self, viewport_api):
    # Update drawing.
    if self._sceneDraw != None:
        self._sceneDraw.invalidate()

_notice_objects_changedで「self._sceneDraw.invalidate()」を呼んでいます。
これは、前述のSceneDrawクラスの再描画を促す処理です。
_notice_objects_changedが呼ばれるタイミングは、「何かPrimが変更された場合」です。
Primの位置が変更された場合、など。
何のアクションもない場合は、これは呼ばれません。

これにより、「何かPrimが変更された場合」にSceneViewを再描画(invalidate)することで、
オーバレイするPrim名描画もマニピュレータの動きに合わせてついていくことができます。

また、_viewport_changedはビューポートの変更があった場合に呼ばれます。
アクティブなカメラが切り替わった場合、カメラの視線が変更された場合など。
このコールバックでは「self._sceneDraw.invalidate()」を呼んでいます。

ただし、これらの処理だけではPrimの選択が変更された場合を監視できません。
そこで、Primの選択が変更された場合の通知はupdateイベントを使って監視することにしました。

Updateイベント

Updateイベントが呼ばれた場合は以下をコールバックします。
これは2つの監視を行う役割があります。

  • 選択Primの切り替えを監視
  • アクティブビューポートの切り替えを監視

もし、上記に変更がある場合に再描画を促します。

    def on_update (self, e: carb.events.IEvent):
        # Check every 0.2 seconds.
        curTime = time.time()
        diffTime = curTime - self._time
        if diffTime > 0.2:
            self._time = curTime

            # Get selection.
            selection = omni.usd.get_context().get_selection()
            paths = selection.get_selected_prim_paths()

            # Selection changed.
            if self._selectedPrimPaths == None or self._selectedPrimPaths != paths:
                self._selectedPrimPaths = paths

                # Update drawing.
                self._sceneDraw.invalidate()

            # If the active viewport name has changed.
            active_vp_window = omni.kit.viewport.utility.get_active_viewport_window()
            if active_vp_window != None and active_vp_window.name != self._active_viewport_name:
                # Rebuild overlay.
                self.term_window()
                self.init_window()

「time.time()」は現在の時間を秒単位で返します。
これの差分を取ると「xx秒経過」を知ることができます。
ここでは0.2秒間隔で「選択が変更されたか」を以下のようにチェックしました。

# Get selection.
selection = omni.usd.get_context().get_selection()
paths = selection.get_selected_prim_paths()

# Selection changed.
if self._selectedPrimPaths == None or self._selectedPrimPaths != paths:
    self._selectedPrimPaths = paths

pathsに選択されたPrimパスがリストとして返ります。
あらかじめPrimパスのリストを保持しておき、まだ取得していない場合/前回と異なる場合は選択が変更された、と判断できます。
このときに「self._sceneDraw.invalidate()」を呼ぶことで、SceneViewを更新としました。

また、

active_vp_window = omni.kit.viewport.utility.get_active_viewport_window()

でアクティブなビューポートを取得し「active_vp_window.name」でビューポート名("Viewport", "Viewport 2"など)が前回と変更されたかチェックしています。
前回と異なるビューポート名の場合、アクティブなビューポートが切り替わったと判断できます。
この場合は、オーバレイの処理自身を切り替える必要があるため「self.term_window()」で破棄して「self.init_window()」で再度初期化しています。

Updateイベントの登録

以下で、Updateイベントが発生した場合は"self.on_update"が呼ばれます。

# Register for update event.
self._subs_update = omni.kit.app.get_app().get_update_event_stream().create_subscription_to_pop(self.on_update)

これは一定時間間隔で常に呼ばれるものと思っていただいて問題ありません(Windows/Linuxで差がありましたが、60fpsとか)。

Updateイベントの解除

保持した「self._subs_update」にNoneを入れるとイベントが破棄されます。

self._subs_update = None

初期化 : Viewportのオーバレイ部

Viewportのオーバレイ部は以下のように記載しました。

    def init_window (self):
        # Get current stage.
        self._stage = omni.usd.get_context().get_stage()

        # Notification of object changes.
        self._objects_changed_listener = Tf.Notice.Register(
            Usd.Notice.ObjectsChanged, self._notice_objects_changed, self._stage)

        # Register for update event.
        self._subs_update = omni.kit.app.get_app().get_update_event_stream().create_subscription_to_pop(self.on_update)

        self._time = time.time()

        # Kit104 : Get active viewport window.
        active_vp_window = omni.kit.viewport.utility.get_active_viewport_window()

        # Get viewport API.
        self._viewport_api = active_vp_window.viewport_api

        # Called when the focus of the viewport changes.
        # The following are disabled because they are unstable.
        #active_vp_window.set_focused_changed_fn(self._focused_changed)

        # Register a callback to be called when the camera in the viewport is changed.
        self._subs_viewport_change = self._viewport_api.subscribe_to_view_change(self._viewport_changed)

        # Get viewport window.
        self._active_viewport_name = active_vp_window.name   # "Viewport", "Viewport 2" etc.
        self._window = omni.ui.Window(self._active_viewport_name)

        with self._window.frame:
            with omni.ui.VStack():
                # The coordinate system is NDC space.
                # (X : -1.0 to +1.0, Y : -1.0 to +1.0).
                self._scene_view = sc.SceneView(aspect_ratio_policy=sc.AspectRatioPolicy.STRETCH)

                with self._scene_view.scene:
                    self._sceneDraw = SceneDraw(self._viewport_api)

                    # Update drawing.
                    self._sceneDraw.invalidate()

「Tf.Notice.Register」でPrimが変更されたことを通知するイベントを登録。
「get_update_event_stream().create_subscription_to_pop」でUpdateイベントを登録。

以下の処理で、アクティブなビューポートを取得してViewport APIを保持しています(Kit.103以降が必要)。

# Kit104 : Get active viewport window.
active_vp_window = omni.kit.viewport.utility.get_active_viewport_window()

# Get viewport API.
self._viewport_api = active_vp_window.viewport_api

以下の処理で、ビューポートのカメラが切り替わった場合などのコールバックを登録しています。Viewport APIのメソッドを使っています。

self._subs_viewport_change = self._viewport_api.subscribe_to_view_change(self._viewport_changed)

なお、「active_vp_window.set_focused_changed_fn」でビューポート含むウィンドウのフォーカスが変更された場合のイベントをコールバックできます。
ただ、これは安定しなかったため、アクティブなビューポートが切り替わったかどうかは定期的にチェックするUpdateイベントで判断するようにしました。

次に、オーバレイ対象となるアクティブなビューポートを取得します。
Omniverse Create 2022.2(Kit.103以降)はビューポートは1つとは限りません。
そのため、"active_vp_window.name"よりビューポート名を取得してこれで判断しています。

# Get viewport window.
self._active_viewport_name = active_vp_window.name   # "Viewport", "Viewport 2" etc.
self._window = omni.ui.Window(self._active_viewport_name)

omni.ui.WindowでViewportのウィンドウを取得します。
以降はオーバレイの実装になります。
「sc.SceneView(aspect_ratio_policy=sc.AspectRatioPolicy.STRETCH)」でオーバレイの描画領域であるSceneViewを取得。
このとき、"aspect_ratio_policy"はSTRETCHとしています。
こうすることで、ビューポートはX方向に-1.0~+1.0、Y方向に-1.0~+1.0とした座標系になります。
これは「NDC座標」となります。
この例ではSceneDrawクラスでの描画部でワールド座標をNDC座標に変換して渡すため、
カメラのView/Projection変換行列をSceneViewに渡す必要はありません。

以下で、描画を行うSceneDrawクラスを作成し、再描画(invalidate)を促しています。
SceneDrawクラスのコンストラクタの第一引数にはViewport APIの参照を渡しました。

with self._scene_view.scene:
    self._sceneDraw = SceneDraw(self._viewport_api)

    # Update drawing.
    self._sceneDraw.invalidate()

この後は、Tf.NoticeやUpdateイベントの通知でSceneDrawクラスでの描画が更新されることになります。

破棄処理

    def term_window (self):
        if self._objects_changed_listener:
            self._objects_changed_listener.Revoke()

        self._window = None
        self._objects_changed_listener = None
        self._subs_update = None
        self._subs_viewport_change = None

登録したイベントの破棄/ViewportのWindowの解放を行っています。

以上で、Omni.UI Sceneを使ったオーバレイで任意のPrimに対して処理を行えるようになりました。