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

  • by

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

Omniverse Codeのドキュメントにある、「Omni.UI Scene」をもう少し深堀りしてみます。
今回は、「Omni.UI Scene」を使ったViewportへのオーバレイ表示についてです。

Kitのバージョンに注意 (2022/12/21 追記)

Omniverse Create 2022.3.1(Kit.104.1)で確認しました。
Omniverse Kitのバージョンにより、Viewport周りは大きく仕様が変わっています。
Omniverse Create 2022.2.x(Kit.103)では、複数のビューポートが管理できるように「Viewport 2.0」という仕組みに移行しており、この段階では" omni.kit.viewport_legacy"というのを使用して古いViewportのAPIはかろうじて動作していました。
Omniverse Create 2022.3.x(Kit.104)ではこのviewport_legacyは削除されたため、新しいViewportの仕様に合わせる必要があります。

GitHubのほうにもビューポートについてまとめていますので、こちらもご参照くださいませ。
https://github.com/ft-lab/omniverse_sample_scripts/tree/main/UI/Viewport

「Omni.UI Scene API Documentation」のドキュメントのソースはどこ ?

「Omni.UI Scene API Documentation」をOmniverse Codeでドキュメントとして見れますが、
部分的にテキストを選択というのができません。
また、プログラムのソースコードは一括コピーになります(部分的に選択ができない)。

ソースの一部をコピペしたい場合、テキストの一部をコピペして翻訳したい場合など、やはりテキスト選択したいです。
このソースは「kit/extscore/omni.ui.scene/docs」内にありました。
Markdown形式のmdファイルがテキストになっています。
これをVSCodeで開いて内容を確認したほうが、いろいろ検証に使えそうです。

いくつかのサンプルは「kit/extscore/omni.ui.scene/omni/ui_scene/tests」にPythonのソースがありました。
ただ、Exampleのサンプル(Viewportのオーバレイのサンプル)はありませんでした。

ビューポートについては https://docs.omniverse.nvidia.com/kit/docs/omni.kit.viewport.docs/latest/overview.html を参考にしたほうがよさそうです。

omni ui.sceneについては https://docs.omniverse.nvidia.com/kit/docs/omni.ui.scene/latest/index.html を参考にしたほうがよさそうです。

Omni.UI Sceneを使ってViewportにオーバーレイする

2022/12/21 : Kit.104でも動作するように修正済み。

Omni.UI Sceneを使ってViewportにオーバーレイするサンプルを作りました。
以下にサンプルExtensionを置いています。
https://github.com/ft-lab/omniverse_sample_scripts/tree/main/Extensions/ft_lab.sample.uiSceneViewportOverlay

このExtensionを実行すると、立方体のワイヤーフレームと原点位置に「Hello Omniverse !!」というテキストを表示します。

ExtensionをOffにすると終了します。

Omni.Ui Scene API DocumentationのExampleを参考にしましたが、いくつか補間しています。

必要な情報を列挙します。
"extension.py"に必要なコードは全部入れてます。

ヘッダ部

from pxr import Usd, UsdGeom, CameraUtil, UsdShade, 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

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

"omni.kit.viewport.utility"からアクティブなビューポートを取得できます。
これはKit.103で追加された機能です。

カレントカメラのPrim Pathを取得

カレントのカメラパス("/OmniverseKit_Persp"など)は以下のように取得できます。

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

# Get camera path ("/OmniverseKit_Persp" etc).
self._camera_path = self._viewport_api.camera_path.pathString

"omni.kit.viewport.utility.get_active_viewport_window()"でアクティブなビューポートウィンドウを取得します。
Kit.103以降は複数ビューポートを持つことができるため、このメソッドを経由する必要がありました。
"active_vp_window.viewport_api"はビューポート情報を取得するAPIのクラスです。
Viewport APIについては https://docs.omniverse.nvidia.com/kit/docs/omni.kit.viewport.docs/latest/viewport_api.html が参考になります。

結果としてカレントカメラのPrimパスが返されます。
デフォルトは"/OmniverseKit_Persp"。
オブジェクトカメラをアクティブにしている場合は、そのときのPrim Pathが返されることになります。

カレントカメラからViewMatrix/ProjectionMatrixを取得

def getCurrentCameraViewProjectionMatrix (self):
    # Get current camera Prim Path.
    if self._camera_path == None:
        self._camera_path = self._viewport_api.camera_path.pathString

    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

カレントのカメラのパス(ここでは「self._camera_path」)を使い、
それぞれのカメラを取得してViewMatrixとProjectionMatrixを取得します。
これは"Gf.Matrix4d"です。
この変換行列をsc.SceneViewのModelに渡すことで、SceneViewをUSDのカメラのビューに合わせています。

カメラの変更を通知 (Tf.Noticeを使用)

シーン上の何かしらの変更を知るには、USDの機能である「Tf.Notice」を使うのが便利です。
カレントカメラのPrimパスはあらかじめ「self._camera_path」に保持しており、
これの変更があった場合に"self._camera_changed()"を呼んでいます。
※ ただし、アクティブなカメラが変わった場合、Stageが変わった場合はTf.Noticeでは追えないようでした。

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):
    # Camera changed.
    self._camera_path = self._viewport_api.camera_path.pathString

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

_camera_changed()では、カレントカメラのView/Projection Matrixを取得し、
これを"self._scene_view.model"に格納しています。
_scene_viewは後述します。
このとき、Gf.Matrix4dのデータを一次配列に変換するためにflattenメソッドを呼んでいます。

Tf.Noticeの登録

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

# Tracking the camera
self._objects_changed_listener = Tf.Notice.Register(
    Usd.Notice.ObjectsChanged, self._notice_objects_changed, self._stage)

"Tf.Notice.Register"を使い、第一引数を"Usd.Notice.ObjectsChanged"として第二引数のメソッドを呼ぶことになります。
第三引数はカレントStageを渡しています。
"Usd.Notice.ObjectsChanged"はUSDのPrimに変化があった場合に呼ばれます。

Tf.Noticeの説明については、USDのドキュメントが参考になります。

https://graphics.pixar.com/usd/release/api/page_tf__notification.html

Tf.Noticeの解放

Tf.Notice.Registerの戻り値を"self._objects_changed_listener"としたとき、以下のように解放します。

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

オーバレイ部の描画

Viewportの取得は"self._window = omni.ui.Window('Viewport')"を使いました。
以下、Viewportにオーバレイして描画する処理です。


# 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():
        self._scene_view = sc.SceneView()

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

        with self._scene_view.scene:
            # Edges of cube
            cubeSize = 100.0
            sc.Line([-cubeSize, -cubeSize, -cubeSize], [cubeSize, -cubeSize, -cubeSize])
            sc.Line([-cubeSize, cubeSize, -cubeSize], [cubeSize, cubeSize, -cubeSize])
            sc.Line([-cubeSize, -cubeSize, cubeSize], [cubeSize, -cubeSize, cubeSize])
            sc.Line([-cubeSize, cubeSize, cubeSize], [cubeSize, cubeSize, cubeSize])

            sc.Line([-cubeSize, -cubeSize, -cubeSize], [-cubeSize, cubeSize, -cubeSize])
            sc.Line([cubeSize, -cubeSize, -cubeSize], [cubeSize, cubeSize, -cubeSize])
            sc.Line([-cubeSize, -cubeSize, cubeSize], [-cubeSize, cubeSize, cubeSize])
            sc.Line([cubeSize, -cubeSize, cubeSize], [cubeSize, cubeSize, cubeSize])

            sc.Line([-cubeSize, -cubeSize, -cubeSize], [-cubeSize, -cubeSize, cubeSize])
            sc.Line([-cubeSize, cubeSize, -cubeSize], [-cubeSize, cubeSize, cubeSize])
            sc.Line([cubeSize, -cubeSize, -cubeSize], [cubeSize, -cubeSize, cubeSize])
            sc.Line([cubeSize, cubeSize, -cubeSize], [cubeSize, cubeSize, cubeSize])

            # Use Transform to change position.
            moveT = sc.Matrix44.get_translation_matrix(0, 0, 0)
            with sc.Transform(transform=moveT):
                sc.Label("Hello Omniverse !!", alignment = omni.ui.Alignment.CENTER, color=cl("#ffff00a0"), size=20)

"omni.ui.Window"の第一引数に渡すビューポート名は、マルチビューポートに対応するため「アクティブなビューポート名」を取得する必要があります。
"omni.kit.viewport.utility.get_active_viewport_window()"で取得したクラスからnameを取得することで、アクティブなビューポート名を得ることができます。

self._window(Viewport)に対して描画を行います。
omni.ui.VStack()は垂直方向の配置。
"self._scene_view = sc.SceneView()"でomni.uiのsceneよりSceneViewを保持しています。
引数は指定していません。これによりViewport全体を描画領域としています。

この段階ではSceneViewのmodelにカメラのview/projection変換行列が渡っていないため、
"self._camera_changed()"を呼んでmodelを更新しました。

「with self._scene_view.scene」とすることで、以降は描画処理を行います。

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

        with self._scene_view.scene:
            # 何か描画処理

sc.Lineを使い、3D空間上に立方体のワイヤーフレームを描画してます。

sc.Line([始点のXYZ], [終点のXYZ])

以下で、原点位置を中心に"Hello Omniverse !!"というテキストを描画しています。

# Use Transform to change position.
moveT = sc.Matrix44.get_translation_matrix(0, 0, 0)
with sc.Transform(transform=moveT):
    sc.Label("Hello Omniverse !!", alignment = omni.ui.Alignment.CENTER, color=cl("#ffff00a0"), size=20)

「color=cl("#ffff00a0")」はRGBAの色指定です。アルファを16進数の"a0"としてます。
"ff"で完全不透明です。アルファを使用しない場合は「color=cl("#ffff00")」でもOKです。
「size=20」はフォントサイズの指定です。

SceneViewではカメラのView/Projection Matrixを指定することで、3D空間のビューになります。
この情報はTf.Notice経由のself._camera_changedで

if self._scene_view != None:
    self._scene_view.model.view = view
    self._scene_view.model.projection = projection

として格納してます。

これにより、Viewport上にオーバレイする形で3次元情報として描画を行えることになります。

既知の問題

引き続き追跡予定です。

  • "omni.kit.viewport_legacy"を経由しないカレントカメラの取得 対応済み
  • Tf.Noticeで、カレントカメラが変更された場合に追えない 対応済み
  • Tf.Noticeで、Stageが変更された場合に追えない

このオーバレイを使用することで、選択されたPrimにテキストを表示したり、独自マニピュレータを表示/編集に使用、などができるようになります。

今回のこの実装では、Scene Viewに対してカメラと同じView/Projection行列を指定しています。
つまり、ワールド座標の値を入れることでオーバーレイとして描画されます。

https://github.com/ft-lab/omniverse_sample_scripts/tree/main/Extensions/ft_lab.sample.uiSceneShowPrimName 」のサンプルExtensionは、
選択PrimにPrim名を表示する実装になります。
このサンプルでは「NDC座標」というビューポート全体をX方向に-1.0から+1.0、Y方向に-1.0から+1.0としたときの座標系で指定するようにしています。
これらの座標系については「 https://github.com/ft-lab/omniverse_sample_scripts/tree/main/UI/Viewport 」にも記載していますので参考になるかと思います。
このサンプルExtensionはブログの「[Omniverse] 「Omni.UI Scene」でViewportにオーバレイ その2」で解説しています。