0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

USD(Universal Scene Description)をローカルビルドして、Viewerをカスタムしてみる

Posted at

はじめに

USD(Universal Scene Description)Viewerを触ることがあったので、色々まとめたいと思います。
会社でUSD Viewerを導入するにあたって、まずは手始めにスタンドアローンでプレビューするのに使ったり。または、モジュールとして組み込みで使える様になどを行っていました。
最初は訳わかんなった(ビルド)ですけど、僕みたいなエンジニアではない人や、USDこれから触ろうとしている人に向けて記録として残しておきます。
ゴールとしては、ビルドしたものをカスタムしてViewerをコントロールするところまでとします。

とりあえず、サンプルのデータ見てみたいという方は、以下レポをご覧ください。
https://github.com/testkun08080/custom-embed-usdviewer

作成される最終イメージ

イメージ

この記事の流れ

  1. Universal Scene Description (USD) とは?
  2. ローカルでビルドする(python)
  3. USD viewer単独で起動する
  4. USD Viewerをカスタムしてみる

Universal Scene Description (USD) とは?

ざっくり言えば、、、複雑になった3Dアセットのやり取りを、USDが中間に入って円滑にしよう!
という解釈でいいと思います。以下「USDとは」を公式から翻訳したもの。

映画やゲームなどのコンピューターグラフィックスを制作するパイプラインでは、大量の3Dデータ(シーン記述)を生成、保存、送信する必要があります。これらのデータは、モデリング、シェーディング、アニメーション、ライティング、エフェクト、レンダリングなど、各アプリケーションの特定のニーズに応じてカスタマイズされており、他のアプリケーションでは読み込みや編集ができないことが一般的です。

Universal Scene Description (USD) は、この課題を解決するために初めて公開されたソフトウェアであり、複数の要素で構成された3Dシーンを効率的かつスケーラブルに共有し、拡張するための強力なツールです。

USDはモデルやアニメーションなどの要素資産を交換するだけでなく、それらを仮想セット、シーン、ショット、ワールドにまとめ、各アプリケーション間で送信し、破壊的でない方法で編集することも可能です。さらに、単一のAPIとシーングラフを使用して、3Dジオメトリ、シェーディング、ライティング、物理シミュレーションなど、さまざまなグラフィック関連ドメインを迅速にプレビューしたり編集したりするための豊富なツールセットを提供します。

特に、USDのコアであるシーングラフとコンポジションエンジンは、特定のドメインに依存しない設計となっており、他のデータドメインを持続可能な形で拡張することも可能です。

USDはオープンソースプロジェクトであり、TOSTライセンスのもとで提供されています。1

開発環境

  • macOS Sequoia 15.5
  • VsCode
  • zsh 5.9 (arm64-apple-darwin24.0)

ローカルでビルドしてみる

  • まずはローカルへクローンまたはサブモジュールを行います
    サブモジュールを使う場合

    git submodule add https://github.com/PixarAnimationStudios/OpenUSD OpenUSD         
    

    or

    クローン

    git clone https://github.com/PixarAnimationStudios/OpenUSD.git        
    
  • UVを使用してVENVの作成し、ビルド開始
    よく公式のドキュメントをご覧になって、ビルドしてみてください。

    uv init -p 3.11
    uv add PyOpenGL PySide6 numpy
    uv run OpenUSD/build_scripts/build_usd.py BuildUSD
    

    ビルドが正しく終われば以下のようなログが見れるはずです。

    Success! To use USD, please ensure that you have:
    
      The following in your PYTHONPATH environment variable:
      /Users/Username/path/custom-embed-usdviewer/BuildUSD/lib/python
    
      The following in your PATH environment variable:
      /Users/Username/path/custom-embed-usdviewer/BuildUSD/bin
    
  • 環境パスの設定

    PYTHONPATHの設定

    cat > .env <<EOL
    PYTHONPATH=./BuildUSD/lib/python
    EOL
    

    PATHの設定

    export PATH=./BuildUSD/bin:$PATH
    

USD viewer単独で起動する

以下コマンドで、USDViewerが立ち上がるはずです。(初回は起動に時間がかかるはずです)

  • uvでusdviewを直接起動
    uv run --env-file=.env usdview OpenUSD/extras/usd/tutorials/convertingLayerFormats/Sphere.usda
    

Python経由で起動する場合

  • pythonファイル経由で起動
    • ファイル作成

      cat > open_usd_viewer.py <<EOL
      """This script is a simple USD viewer using the pxr.Usdviewq module."""
      import sys
      import pxr.Usdviewq as Usdviewq
      
      if __name__ == "__main__":
          # Let Ctrl-C kill the app.
          import signal
      
          signal.signal(signal.SIGINT, signal.SIG_DFL)
      
          try:
              usd_path = "assets/Sphere.usda"
              sys.argv.append(usd_path)
              Usdviewq.Launcher().Run()
          except Usdviewq.InvalidUsdviewOption as e:
              print("ERROR: " + str(e), file=sys.stderr)
              sys.exit(1)
      EOL
      
    • 作成後、起動

      uv run open_usd_viewer.py
      

USD Viewerをカスタムしてみる

usdviewerを埋め込んだwidgetを作成

まずは、usdviewerを埋め込んだwidgetを作成します。
コードを見ていただければわかると思いますが、from pxr.Usdviewq.stageView import StageViewが重要なところかと思います。
あとラッパー関数を作っておいて、後に記述するUI側で操作する感じです。
以下コマンドをコピーしてファイルを作成できます

embed_usd_widget.py
cat > embed_usd_widget.py <<EOL
"""A simple application to embed a USD viewer using PySide6."""

import os
import sys
from datetime import datetime
from PySide6 import QtCore, QtWidgets
from pxr import Usd, UsdUtils
from pxr.Usdviewq.stageView import StageView, RenderModes


class EmmbedUSDWidget(QtWidgets.QWidget):
    """A widget to embed USD stage viewer."""

    def __init__(self, stage=None):
        super(EmmbedUSDWidget, self).__init__()
        self.model = StageView.DefaultDataModel()
        self.stage = stage
        self.view = StageView(dataModel=self.model)

        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.view)
        layout.setContentsMargins(0, 0, 0, 0)

        if self.stage:
            self.set_stage(self.stage)

        # Reset View
        self.view.updateView(resetCam=True, forceComputeBBox=True)

    def image_save(self):
        """Save the current view as an image file."""
        print("GetRendererAovs", self.view.GetRendererAovs())
        image = self.view.grabFramebuffer()
        timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
        os.makedirs("screenshots", exist_ok=True)
        filename = f"screenshots/screenshot_{timestamp}.png"
        image.save(filename)

    def set_stage(self, stage):
        """Set the USD stage for the viewer

        Args:
            stage (QGLWidget): USD stage to set in the viewer.

        """
        self.model.stage = stage

    def set_render_mode(self, mode):
        """Set the render mode for the viewer.

        Args:
            mode (str): The render mode to set.
        """
        if mode in RenderModes:
            self.model.viewSettings.renderMode = mode
        else:
            print(f"Invalid render mode: {mode}")

    def reset_camera(self):
        """Resets the camera to fit the geometry in the view."""
        self.view.updateView(resetCam=True, forceComputeBBox=True)

    def show_hud(self, enable=True):
        """Show the HUD (Heads-Up Display) in the viewer.

        Args:
            enable (bool, optional): A value will handle HUD . Defaults to True.
        """

        self.model.viewSettings.showHUD = enable

    def show_bboxes(self, enable=True):
        """Show bounding boxes in the viewer.

        Args:
            enable (bool, optional): A value will handle bounding boxes. Defaults to True.
        """

        self.model.viewSettings.showBBoxes = enable

    def closeEvent(self, event):
        """Close envet handler to ensure proper cleanup.

        Args:
            event (QtGui.QCloseEvent): The close event triggered by the widget.
        """

        # Ensure to close the renderer to avoid GlfPostPendingGLErrors
        self.view.closeRenderer()

if __name__ == "__main__":
    app = QtWidgets.QApplication([])

    import signal

    signal.signal(signal.SIGINT, signal.SIG_DFL)

    path = "assets/Sphere.usda"

    with Usd.StageCacheContext(UsdUtils.StageCache.Get()):
        stage = Usd.Stage.Open(path)

    window = EmmbedUSDWidget(stage)
    window.setWindowTitle("USD Viewer")
    window.resize(QtCore.QSize(750, 750))
    window.show()

    sys.exit(app.exec_())
EOL

作ったwidgetを使ってカスタムviewerを作る

カスタムといっても、ドッキング可能なwidgetから、viewerのUIをラップー関数を使って操作する簡単なwindowを作ります。
構造としては、大まかなUIファイルで作成しておいて、埋め込みwidgetを使っているだけです。

UIファイル

以下イメージのようにQTCreatorなどでファイルを作成します。
イメージ
uiファイルの詳細はgit内のファイルをご確認ください。
https://github.com/testkun08080/custom-embed-usdviewer/blob/main/UI/usdViewerController.ui

以下コマンドをコピーしてUIファイルをダウンロードしできます

mkdir -p UI && curl -L -o UI/usdViewerController.ui https://raw.githubusercontent.com/testkun08080/custom-embed-usdviewer/main/UI/usdViewerController.ui

UIファイルを使って埋め込み用usdviewer widgetを操作する

以下コマンドをコピーしてファイルを作成できます

app.py
cat > app.py <<EOL
"""Module providing a USD viewer application."""

import sys
from PySide6 import QtWidgets
from PySide6.QtUiTools import QUiLoader
from PySide6.QtCore import QFile, QIODevice
from pxr import Usd, UsdUtils
from pxr.Usdviewq.stageView import RenderModes

from embed_usd_widget import EmmbedUSDWidget


def get_ui_widget(ui_file_name):
    """Loads and returns a UI widget from a UI file.

    Args:
        ui_file_name (str): The path to the UI file to load.

    Returns:
        QWidget: The loaded UI widget.
    """
    ui_file = QFile(ui_file_name)
    if not ui_file.open(QIODevice.ReadOnly):
        print(f"Cannot open {ui_file_name}: {ui_file.errorString()}")
        sys.exit(-1)
    loader = QUiLoader()
    ui_widget = loader.load(ui_file)
    ui_file.close()

    return ui_widget


class EmbedUsdViewerController:
    """Controller class for the Embed USD viewer.

    Manages the loading of USD stages and the viewer widget.
    """

    def __init__(self, usd_file=None):
        """Initializes the USD viewer controller.

        Args:
            usd_file (str): USD file
        """

        self.ui = get_ui_widget("UI/usdViewerController.ui")

        # Load USD Stage
        with Usd.StageCacheContext(UsdUtils.StageCache.Get()):
            self.stage = Usd.Stage.Open(usd_file)

        # Embed USD Viewer widget
        self.usd_widget = EmmbedUSDWidget(self.stage)
        self.ui.main_layout.addWidget(self.usd_widget)

        # Connect Save Image button event
        self.ui.save_image_button.clicked.connect(self.usd_widget.image_save)

        # Connect Reset Camera button event
        self.ui.reset_camera_button.clicked.connect(self.usd_widget.reset_camera)

        # Connect Render Mode comboBox event
        self.ui.shading_mode_combobox.addItems([mode for mode in RenderModes])
        self.ui.shading_mode_combobox.setCurrentText(self.usd_widget.model.viewSettings.renderMode)
        self.ui.shading_mode_combobox.currentTextChanged.connect(self.usd_widget.set_render_mode)

        # Connect HUB checkBox event
        self.ui.show_hud_checkbox.setChecked(self.usd_widget.model.viewSettings.showHUD)
        self.ui.show_hud_checkbox.clicked.connect(self.usd_widget.show_hud)

        # Connect BBoxes checkBox event
        self.ui.show_bbxs_checkbox.setChecked(self.usd_widget.model.viewSettings.showBBoxes)
        self.ui.show_bbxs_checkbox.clicked.connect(self.usd_widget.show_bboxes)

    def setup_aov_ui(self):
        """Sets up the UI for AOVs selection.

        Retrieves the AOVs list and adds them to a combo box.
        """

        # Retrieve and add AOVs list to combo box
        aovs = self.usd_widget.view.GetRendererAovs()
        if not aovs:
            # Add default AOVs if the list is empty
            default_aovs = ["color", "primId", "depth", "Neye"]
            for aov in default_aovs:
                self.ui.aovs_combobox.addItem(aov)
        else:
            for aov in aovs:
                self.ui.aovs_combobox.addItem(aov)

        # Connect AOV selection event
        self.ui.aovs_combobox.currentTextChanged.connect(self.on_aov_changed)

    def on_aov_changed(self, aov_name):
        """Updates the view based on the selected AOV.

        Args:
            aov_name (str): The name of the selected AOV.
        """

        # Update view to the selected AOV
        if aov_name:
            self.usd_widget.view.SetRendererAov(aov_name)
            self.usd_widget.view.update()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)

    viewer_controller = EmbedUsdViewerController("assets/Sphere.usda")
    viewer_controller.ui.show()

    # You might need to call this after shop up the window
    viewer_controller.setup_aov_ui()
    sys.exit(app.exec_())
EOL

app.pyとuiファイルの作成が終わったら、以下コマンドで起動してみてください。

uv run app.py

うまくいけば、以下のようなUIが起動するはずです。
最終形態

まとめ、感想

最後までご覧いただきありがとうございました。
各会社、独自でゲームエンジンなどを持つところは、これを使ってプレビューツールとして外部へ渡したりなども可能かなと考えています。
(大変そうだけども)
いいね頂けると一日ハッピーです。

参考文献

  1. USDとは(https://openusd.org/release/intro.html#what-is-usd)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?