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

  • by

Omniverse Codeのドキュメントにある、「Omni.UI Scene」をもう少し深堀りしてみます。
Omniverse Code 2022.1.2で確認しました。
また、Omniverse Create 2022.1.4でも動作確認しました。

今回は、「Omni.UI Scene」を使った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のオーバレイのサンプル)はありませんでした。

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

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を参考にしましたが、いくつか補間しています。
Viewportからカメラの取得はLegacyを使ったため、作法としてはお行儀が悪いかもしれません。

必要な情報を列挙します。
"main.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

カレントカメラの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

カレントカメラを「omni.kit.viewport_legacy」を使ってここから取得しました。
ただ、Legacyのため今後使用できなくなる可能性があります。
これの代わりの取得方法は調査中。

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

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

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

カレントのカメラのパス(ここでは「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.getCurrentCameraPrimPath()

    # 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 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:
            # 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)

以下で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にテキストを表示したり、独自マニピュレータを表示/編集に使用、などができるようになります。

これらの情報は引き続きブログで書く予定です。