Omniverse Kit App Templateを使ったアプリ開発の流れ - Extensionの開発とサンプルプログラム実装 -

前回の記事ではOmniverseでのKit App Templateを使ったアプリの構築・ビルド・起動までの流れをご紹介しました。

https://qiita.com/GIZAGIZAHEART/items/37b05ae9d0bd5808588a


今回はExtensionを作りオリジナルの機能を作ってみましょう。
前回と同様、またkit-app-templateの下でrepo.bat template newを実行します。
```
repo.bat template new
```

ナビゲーションで、今後はExtensionを選びます。

- Extension Templateを選びます。今回はPythonを使ったサンプル実装を行うため、Basic Python Extensionを選択します。
- Extensionの名前を入力します。
- Extensionの表示名を入力します。
- バージョンを入力します。

Extensionを作ることができました！

![スクリーンショット 2025-01-02 8.56.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111483/a5444425-8892-d101-607f-805a51f62609.png)


エクスプローラー上で新しいExtensionの関連ファイルが
作成されていることを確認できます。

![スクリーンショット 2025-01-02 8.57.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111483/c3999158-d371-f02d-5000-4570ca652eb8.png)


次にExtensionのサブディレクトリの中にあるExtension.pyを開きます。
この中にはpython言語でデフォルトのExtension用のコードが記述されています。

![スクリーンショット 2025-01-02 8.57.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111483/caf7b24f-aeb3-a930-97f1-02541ec5f61a.png)


この中で新しいExtesion用のコードを実装していきましょう。

ここでは独自ウィンドウで
Hello Omniverse!と表示するExtensionを作ります。

UIのサンプルコードを参考にして、python プログラムを実装してください。
on_startup関数はアプリが作成され、Extensionを作成しtらちじゅ

```
import omni.ui as ui
```


```
def on_startup(self, _ext_id):
    """This is called every time the extension is activated."""
    print("[my_company.my_python_extension] Extension startup")

    self._window = ui.Window("My hoge", width=300, height=200 )
        with self._window.frame:
            with ui.VStack():
                ui.Label("Hello, Omniverse!")
```

```
def on_shutdown(self, _ext_id):
    if self._window:
        self._window.destroy()
        self._window = None

```


書き終えたら、.kitファイルにこのExtension
の依存関係について追記します。

![スクリーンショット 2025-01-02 9.11.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111483/94e1fd1a-63bb-22e8-0ecb-5d55588b4917.png)


```
"my_company.my_python_extension" = {}
```



書き終わったら、repo.bat buildを実行します。

```
repo.bat build
```

ビルドが終わったらアプリケーションを起動します。
```
repo.bat launch
```
Applicationで確認してみましょう。
先ほど実装したウィンドウとテキストが
表示されていることが確認できます。

![スクリーンショット 2025-01-02 9.57.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111483/eabb89fd-d3d9-eaac-88ea-125f1dc156dd.png)

さらにステップアップし、StageにあるUSDの操作をするようなサンプルプログラムを作ってみましょう。


```
import omni.ui as ui
import omni.ext
import omni.usd
import omni.kit.commands
import carb
from pxr import Usd, UsdGeom, Sdf, Gf


# Functions and vars are available to other extensions as usual in python:
# `my_company.my_python_extension.some_public_function(x)`
def some_public_function(x: int):
    """This is a public function that can be called from other extensions."""
    print(f"[my_company.my_python_extension] some_public_function was called with {x}")
    return x**x


# Any class derived from `omni.ext.IExt` in the top level module (defined in
# `python.modules` of `extension.toml`) will be instantiated when the extension
# gets enabled, and `on_startup(ext_id)` will be called. Later when the
# extension gets disabled on_shutdown() is called.
class MyExtension(omni.ext.IExt):
    """This is a blank extension template."""
    # ext_id is the current extension id. It can be used with the extension
    # manager to query additional information, like where this extension is
    # located on the filesystem.
    def on_startup(self, _ext_id):
        """This is called every time the extension is activated."""
        print("[my_company.my_python_extension] Extension startup")


        self._window = ui.Window("Local Object Mover", width=300, height=100)
        with self._window.frame:
            with ui.VStack():
                self._button = ui.Button("Move Object", clicked_fn=self.on_button_click)

        self._usd_context = omni.usd.get_context()


    def on_button_click(self):
        self._stage = omni.usd.get_context().get_stage()
        # Get default prim.
        defaultPrim = self._stage.GetDefaultPrim()
        # Default prim path.
        defaultPrimPath = defaultPrim.GetPath().pathString
        print("DefaultPrim : " + defaultPrimPath)

        # Cone path.
        orgPath = "/World/Cone"
        cone_prim = self._stage.GetPrimAtPath(orgPath)
        translate_attr = cone_prim.GetAttribute("xformOp:translate")

        if not translate_attr:
            translate_attr = cone_prim.CreateAttribute("xformOp:translate", Sdf.ValueTypeNames.Float3)
            translate_attr.Set(Gf.Vec3f(0.0, 0.0, 0.0))

        current_translate = translate_attr.Get()
        # Move the cube by 10 units on the X axis
        new_translate = Gf.Vec3f(current_translate[0] + 10.0, current_translate[1], current_translate[2])

        translate_attr.Set(new_translate)
        print(f"Moved cube to: {new_translate}")

    def on_shutdown(self):
        """This is called every time the extension is deactivated. It is used
        to clean up the extension state."""
        print("[my_company.my_python_extension] Extension shutdown")
        if self._window:
            self._window.destroy()
            self._window = None
```

## importの内容

### omni.ui
Omniverse Kit におけるUI（ユーザーインターフェイス）構築用のモジュールです。Window や Button など、基本的なUIパーツを提供します。
### omni.ext
Omniverseの拡張機能を作る際に必要なインターフェイスを提供するモジュールです。拡張は omni.ext.IExt を継承して実装します。
### omni.usd
USD のステージ（シーンデータ）を扱うための Omniverse 固有のヘルパーモジュールです。get_context() を使うことでステージへのアクセスが簡単に行えます。
### omni.kit.commands
Omniverse Kit 内で提供される各種コマンドを使うためのモジュールです。UI 操作などからコマンドを実行して履歴管理・アンドゥ/リドゥを行う場合などに利用します。
###carb
NVIDIA の低レベルライブラリ群（カーボン）のモジュールです。ログ出力やプラグイン管理など、Omniverse Kit の内部機能と連携する際に使用します。

### pxr のモジュール (Usd, UsdGeom, Sdf, Gf)
Pixar が開発した USD (Universal Scene Description) を扱うための低レベルAPIです。シーン内の各種プリム（オブジェクト）や、Sdf で定義されるパス、Gf の線形代数機能（ベクトルや行列など）を使用します。


## USDの位置更新のための実装

今回はボタンを押した時に、Stageの中にあるUSDを動かすためのコールバック関数をon_shutdown(self)で実装しています。その中の処理について解説していきます。

### USD ステージの取得
omni.usd.get_context().get_stage() で、現在のステージ（シーン）の参照を取得します。
### Default Prim の確認
defaultPrim = self._stage.GetDefaultPrim() でステージ内のデフォルトプリムを取得し、パスをコンソールに出力しています。ここでは動作確認程度で実際の処理には使っていません。

### Cone Prim の取得
今回は先ほどのようなConeオブジェクトを動かすサンプルとします。
orgPath = "/World/Cone" として、シーン内の /World/Cone にあるプリム（円錐）を取得します。これを cone_prim として扱い、移動属性（xformOp:translate）を操作しています。

### xformOp:translate 属性
USD のトランスフォームは通常 xformOp:translate や xformOp:rotateXYZ、xformOp:scale といった形でプリムに設定されます。ここでは GetAttribute("xformOp:translate") で既に存在しているかを確認し、なければ CreateAttribute() で新たに作成しています。Sdf.ValueTypeNames.Float3 を使うことで、「ベクトル型の属性である」という定義を行っています。

### Gf.Vec3f
Pixar USD のライブラリである Gf（geometry foundation）のベクトル型です。Set() でこのベクトルを使って実際のトランスフォーム値を更新します。

### 位置の更新
current_translate で現在の位置を取得し、X 軸方向（配列インデックス [0]）に 10.0 加算して新しい位置ベクトルを作成しています。これにより /World/Cone プリムが X 軸方向に 10 ユニット移動します。

![スクリーンショット 2025-01-02 18.12.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111483/e476398c-9795-7f52-b531-7f1bf324e2d5.png)

なお上記の内容までは下記のYouTubeでご紹介しています。Omniverse kit app templateを使ったアプリ開発の流れについての基本的なチュートリアルを紹介してますので、ぜひご確認ください！

<iframe width="560" height="315" src="https://www.youtube.com/embed/KaBGfy6xDks?si=sVWdmuuIzSLQdPQx" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>