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

  • by

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

以下に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/main.py"が対象の実装になります。

omni.ui.sceneのSceneViewを使ってViewportにオーバレイして描画しています。
また、カメラの移動を「Tf.Notice.Register」を使って「Usd.Notice.ObjectsChanged」を通知し、SceneViewのmodelのview/projection Matrixを更新しています。
これは前回の「[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

選択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してます。

描画処理部

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

class SceneDraw (sc.Manipulator):
    def __init__(self, **kwargs):
        super().__init__ (**kwargs)

    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)

                # Draw prim name.
                moveT = sc.Matrix44.get_translation_matrix(translate[0], translate[1], translate[2])
                with sc.Transform(transform=moveT):
                    sc.Label(prim.GetName(), alignment = omni.ui.Alignment.CENTER, color=cl("#ffff00a0"), size=20)

        #self.invalidate()

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

globalPose = xformCache.GetLocalToWorldTransform(prim)

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

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

SceneViewへの描画として、ここで取得されたワールド座標位置(translate)を与えて、
ラベルとしてPrim名を描画してます。

moveT = sc.Matrix44.get_translation_matrix(translate[0], translate[1], translate[2])
                with sc.Transform(transform=moveT):
                    sc.Label(prim.GetName(), alignment = omni.ui.Alignment.CENTER, color=cl("#ffff00a0"), size=20)

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

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

カメラの変更/Primに何かしら変更があった場合の通知の取得は、以下のように記載しました。

    # ------------------------------------------------.
    # Get current camera prim path.
    # ------------------------------------------------.
    def getCurrentCameraPrimPath (self):
        # Get viewport.
        # Kit103 changed from omni.kit.viewport to omni.kit.viewport_legacy
        viewport = omni.kit.viewport_legacy.get_viewport_interface()
        viewportWindow = viewport.get_viewport_window()

        # Get active camera path.
        cameraPath = viewportWindow.get_active_camera()
        return cameraPath

    # ------------------------------------------------.
    # Get View/Projection Matrix of the current camera.
    # ------------------------------------------------.
    def getCurrentCameraViewProjectionMatrix (self):
        # Get current camera Prim Path.
        if self._camera_path == None:
            self._camera_path = self.getCurrentCameraPrimPath()

        if self._stage == None:
            self._stage = omni.usd.get_context().get_stage()

        viewMatrix = None
        projMatrix = None

        # Get active camera.
        cameraPrim = self._stage.GetPrimAtPath(self._camera_path)
        if cameraPrim.IsValid():
            camera  = UsdGeom.Camera(cameraPrim)                # UsdGeom.Camera
            cameraV = camera.GetCamera(Usd.TimeCode.Default())  # Gf.Camera
            viewMatrix = cameraV.frustum.ComputeViewMatrix()
            projMatrix = cameraV.frustum.ComputeProjectionMatrix()

        return viewMatrix, projMatrix

    # ------------------------------------------------.
    # Camera change event called from Tf.Notice (Usd.Notice.ObjectsChanged).
    # ------------------------------------------------.
    def _camera_changed (self):
        # Called when the camera is changed.
        def flatten (transform):
            # Convert array[n][m] to array[n*m].
            return [item for sublist in transform for item in sublist]

        # Get View/Projection Matrix.
        view, projection = self.getCurrentCameraViewProjectionMatrix()

        # Convert Gf.Matrix4d to list 
        view = flatten(view)
        projection = flatten(projection)

        # Set the scene
        if self._scene_view != None:
            self._scene_view.model.view = view
            self._scene_view.model.projection = projection

    def _notice_objects_changed (self, notice, stage):
        self._camera_path = self.getCurrentCameraPrimPath()

        # Update drawing.
        self._sceneDraw.invalidate()

        # Called by Tf.Notice.
        for p in notice.GetChangedInfoOnlyPaths():
            if p.GetPrimPath() == self._camera_path:
                self._camera_changed()

イベントの登録

イベントの登録は前回と同じです。

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

として何かPrimの変更があった場合に"self._notice_objects_changed"がコールバックされます。

イベントの破棄

イベントの破棄は前回と同じです。

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

self._objects_changed_listener = None

イベントの流れ

「getCurrentCameraPrimPath」で現在アクティブなカメラのPrimパスを取得。
「getCurrentCameraViewProjectionMatrix」でアクティブなカメラのView/Projection変換行列を取得します。

_camera_changed内で「self._scene_view」のmodelのview/projectionにカメラの変換行列を1次配列にして渡しています。
これでSceneViewがViewportに一致することになります。

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

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

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

Updateイベント

Updateイベントが呼ばれた場合は以下をコールバックします。
この中で、選択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()

「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を更新としました。

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()

        # Get current camera Prim Path.
        self._camera_path = self.getCurrentCameraPrimPath()

        # Tracking the camera
        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()

        # Get main window viewport.
        self._window = omni.ui.Window('Viewport')

        with self._window.frame:
            with omni.ui.VStack():
                self._scene_view = sc.SceneView()

                # Update view./projection matrix.
                self._camera_changed()

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

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

「self._window = omni.ui.Window('Viewport')」でViewportのウィンドウを取得。
以降はオーバレイの実装になります。
「self._scene_view = sc.SceneView()」でオーバレイの描画領域であるSceneViewを取得。
この段階ではカメラ情報を渡していないため、続けて「self._camera_changed()」を呼んでカメラ情報をSceneViewのmodelに渡しています。

以下で、描画を行うSceneDrawクラスを作成し、再描画(invalidate)を促しています。

with self._scene_view.scene:
   self._sceneDraw = SceneDraw()
   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

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

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