Unreal Engine のユニバーサル シーン デスクリプション (USD)

ユニバーサル シーン デスクリプション (USD) の概要と、これを Unreal Engine で使用する方法について説明します。

映画やゲームなどの 3D グラフィック プロダクションでは、大量のデータを生成して保存し、送信することがよくあります。これらのデータはアート パイプラインにあるさまざまなソフトウェア (Unreal Engine、Maya、Houdini、Blender など) から送られるもので、シーン デスクリプションはそれぞれ独自の形式で提供されます。

ユニバーサル シーン デスクリプション (USD) 交換形式は、そうした背景から Pixar によって開発されたオープンソースの形式で、多くの要素アセットで構成される可能性のある任意の 3D シーンを、堅牢かつスケーラブルに交換および拡張できるようにするためのものです。USD は、3D ジオメトリとシェーディングの読み取り、書き込み、編集、および迅速なプレビューを行える豊富なツールセットを提供するだけでなく、要素アセット (モデルなど) またはアニメーションの交換も可能です。

また他の交換パッケージとは異なり、USD では、任意の数のアセットを仮想セット、シーン、ショットに組み立てて編成することができます。その後、単一のシーン グラフ内で、単一の一貫した API を使ってアプリケーション間でそれらを送信し、(オーバーライドとして) 非破壊的に編集できます。

USD を使用する理由

USD では、大量のデータを複数の 3D アプリケーション間で移動するための共通言語が提供されます。これにより、アート パイプラインに関してより柔軟に決断を下せるようになり、反復的な方法や非破壊的な方法を使って、複数の 3D アーティスト (アニメーター、ライティング/シェーディング アーティスト、モデラー、FX アーティストなど) によるコラボレーションを促進できます。

Unreal Engine における USD サポート

現時点で、Unreal Engine では USD Stage と双方向の Python ワークフローを通じて USD をサポートしています。

[USD Stage] ウィンドウ。クリックして拡大表示。

スタティック メッシュやマテリアルのようなネイティブの Unreal Engine アセットに USD データを変換するのではなく、USD Stage Actor と USD Stage Editor ウィンドウを使用して、USD Stage ワークフローで USD データをネイティブに操作できます。これにより、USD データをよりすばやく Unreal Engine に取り込んだり、USD コンテンツの元の構造をより明確に表示したり、ディスク上にあるソース USD ファイルに変更を加えた際のライブ アップデート処理を行ったりできるようになります。

USD Stage では次の機能が提供されます。

  • 3D データの「プリミティブ」表現 - スタティック メッシュ、スケルタル メッシュ、HISM、マテリアル、ライト、カメラ、バリアント、アニメーション、ブレンド シェイプ。

  • 属性の非破壊的な編集。

  • USD シーン グラフと階層のビジュアライゼーション。

  • ペイロードを使用したコンテンツのロード/アンロード。

  • USD Preview Surface を使ったテクスチャのサポート。

  • PreviewSurface と DisplayColor によるマテリアルのサポート。

  • Alembic や Custom Path Resolver などの USD プラグインのサポート。

  • ランタイム時における USD 機能のサポート。詳細については、「USD ランタイム サポート」を参照してください。

  • .usd.usda.usdc.usdz 形式のロード。

ユニバーサル シーン デスクリプションの詳細については、Pixar の「Introduction to USD」を参照してください。

Python ワークフローでの作業の詳細については、「Python スクリプティング」を参照してください。

サポートされるアクション

Unreal Engine へのインポート

USD Stage で表示されるコンテンツ (スタティック メッシュスケルタル メッシュライトフォリッジランドスケープ) は、次の方法で Unreal Engine にインポートできます。

  • [File] > [Import Into Level (レベルにインポート)] を使用する。このプロセスでは、アセット (スタティック メッシュ、スケルタル メッシュ、マテリアル、テクスチャなど) とアクタの両方がインポートされます。

  • コンテンツ ブラウザ の [Add/Import (追加/インポート)] ボタンを使用する。このプロセスではアセットのみがインポートされます。

  • ファイルを コンテンツ ブラウザ にドラッグ & ドロップする。このプロセスではアセットのみがインポートされます。

  • USD Stage エディタ[Action] > [Import] オプションを使用する。このプロセスでは、アセットとアクタの両方がインポートされます。インポート プロセスが完了すると、USD Stage のアセットは コンテンツ ブラウザ からの新しいアクタに置換されます。

アニメーションの作成と編集

USD ファイルに格納されているアニメーションには、USDStageActor[Properties (プロパティ)] パネルにある、関連するレベル シーケンスを使ってアクセスできます。

USD Level Stage を選択し、プロパティ パネルのシーケンスをダブルクリックしてレベル シーケンスを開く。 クリックして拡大表示。

レベル シーケンス内の Transform トラックに USD xform のアニメーションが表示されます。その他の形式のアニメーション (floats、boolean、スケルタル ボーンなど) は Time トラックで表示されます。上の画像にある USD アニメーション データは、アニメーションの継続期間の各タイムコードで、キー/値のペアで表現されています。

USD Stage で生成された レベル シーケンス 全体を通じて、USD Stage でスポーンされたアクタにバインドし、Transform トラックでアニメーションを追加することができます。

USD ランタイム サポート

Unreal Engine では、レベル内の USD Stage Actor にある Set Root Layer ブループリント ノードの呼び出しを通じて、ランタイム時における USD ファイルのロードをサポートしています。

Set Root Layer ノード。クリックして拡大表示。

このノードは、エディタでの処理と同じように、必要なアセットを作成して、アクタとコンポーネントをレベル内にスポーンします。USD Stage Actor のさまざまなプロパティを制御するための追加のブループリント関数には次のようなものがあります。

ブループリント関数

説明

Get Generated Assets (生成したアセットを取得)

特定のプリミティブ パス内のプリミティブ用に生成されたアセットを取得し、それを配列に配置します。USD Stage Actor とプリミティブ パスを入力として使用します。

Get Generated Components (生成したコンポーネントを取得)

特定のプリミティブ パス内のプリミティブ用に生成されたコンポーネントを取得します。USD Stage Actor とプリミティブ パスを入力として使用します。

Get Source Prim Path (ソース プリミティブ パスを取得)

USD Stage にある特定のオブジェクト用のプリミティブ パスを取得します。USD Stage Actor とオブジェクト参照を入力として使用します。

Get Time (タイムを取得)

ターゲット USD Stage Actor 内の現在のタイムスタンプを取得します。USD Stage Actor をターゲットとして受け取ります。

Set Initial Load Set (初期ロード セットを設定)

ロードする初期のペイロードを設定します。USD Stage Actor を入力として使用します。次のオプションがあります。

  • Load All (すべてロード):初期にすべてのペイロードをロードします。

  • Load None (ロードしない):初期にペイロードをロードしません。

Set Purpose to Load (ロードの目的を設定)

ロードする初期の Purpose (目的) を設定します。USD Stage Actor と整数値を入力として使用します。

  • 0 = Default (デフォルト)

  • 1 = Proxy (プロキシ)

  • 2 = Render (レンダリング)

  • 3 = Guide (ガイド)

Set Render Context (レンダリング コンテキストを設定)

USD Stage のレンダリング コンテキストを設定します。USD Stage Actor をターゲットとして受け取り、レンダリング コンテキストへの参照を入力として受け取ります。

Set Time (タイムを設定)

USD Stage の現在のタイムスタンプを設定します。USD Stage Actor をターゲットとして受け取り、浮動小数値を入力として受け取ります。

Purpose 属性やその他の USD 用語の詳細については、Pixar の「USD Glossary」を参照してください。

このプロセスを使用することで、USD ファイルのコンテンツをランタイム時にロードして表示できるアプリケーションを作成できます。

ランタイム時に USD インポータを有効にするには、「UE_(version)\Engine\Source」フォルダにある「Project.Target.cs」ファイルに次の行を追加します (「Project」は対象のプロジェクトの名前です)。

GlobalDefinitions.Add("FORCE_ANSI_ALLOCATOR=1");

例:

    public class YourProjectTarget : TargetRules
    {
    public YourProjectTarget( TargetInfo Target) : base(Target)
    {
     Type = TargetType.Game;
     DefaultBuildSettings = BuildSettingsVersion.V2;
     ExtraModuleNames.AddRange( new string[] { "YourProject" } );

     GlobalDefinitions.Add("FORCE_ANSI_ALLOCATOR=1");
    }
    }

Nvidia MDL のサポート

Unreal Engine では、Nvidia MDL USD スキーマを使って MDL サーフェス マテリアルをサポートしています。Nvidia MDL の詳細については、Nvidia の「USD Shader Attributes」を参照してください。

マルチ ユーザー編集のサポート

マルチ ユーザー編集は、以下を含む多くの USD Stage の操作でサポートされています。

  • プリミティブの追加と削除。

  • プリミティブの名前変更。

  • プリミティブの属性の編集。

  • 可視性の切り替え。

  • 現在のステージを開く、閉じる、または変更する操作。

USD プロジェクトでマルチ ユーザー編集を有効にするには、USD Multi-User synchronization プラグインを有効にします。

USD のマルチ ユーザー編集では、各クライアントの USD Stage の [Root Layer (ルート レイヤー)] プロパティが同期されて、すべてのユーザーが同じ USD ファイルを使用するようになります。具体的には、各クライアントで同じ USD Stage をローカルで開いて、それぞれ独自のシステムでアセットとコンポーネントをスポーンし、最後にこれらのアセットに対して行われた操作のみを同期します。

マルチ ユーザー編集セッション中は、すべてのユーザーが同じファイル パスを使って USD ファイルにアクセスすることが重要です。それぞれのクライアントで同じファイルにアクセスできることを確実にするために、ターゲット USD ファイルをプロジェクト フォルダ内に格納し、ソース コントロールを使って管理することをお勧めします。

Unreal Engine でのマルチ ユーザー編集の詳細については、「マルチ ユーザー編集」を参照してください。

現時点では、マルチ ユーザー セッション中に削除したプリミティブを元に戻すことはできません。

USD インポート プラグインを有効にする

Unreal Editor で USD ファイルを扱うには、[Plugins] メニューから USD Importer プラグインを有効にする必要があります。プラグインを使用する方法の詳細については、「プラグインを操作する」を参照してください。

エディタを再起動すると、[Window (ウィンドウ)] メニューに新しく [USD Stage Editor] オプションが表示されます。

[Place Actors (アクタを配置)] パネルには、レベルに追加可能な新しい USD アクタがいくつか表示されます。

USD Place Actors

[Place Actors] パネルに追加された新しい USD アクタ。

USD を Unreal Engine 使用する

Unreal Engine で USD コンテンツの作業を行う際は、USD Stage Editor と USD Stage Actor を使用します。

USD Stage パネル

番号

説明

1

階層

2

プロパティ

3

レイヤー

USD Stage のワークフロー

USD Stage Actor は、ロードされた USD ファイルのコンテンツを格納するコンテナとして機能し、レベル内におけるそのデータのアンカーを提供します。USD ファイルからロードされ、ビューポートに表示される 3D シーン オブジェクトは、Unreal Engine のほとんどの機能と完全な互換性があり、他のアクタと同じように扱うことができます。他の USD ファイルに存在するコンテンツ (アニメートされたスケルタル メッシュを含む) を参照するプリミティブをさらに追加することもできます。

USD Stage に加えた変更を USD Stage Editor の [File] > [Save] メニューを使って保存すると、これらの変更が USD ファイルに書き戻されます。

USD Stage での作業の詳細については、「USD Stage Editor クイック スタート」を参照してください。

Unreal Engine では、USD ファイルを開いた際に USD Stage にロードされたアセットのライトマップを自動的に作成しません。このため、静的ライトをビルドした際にシーンが真っ暗になることがあります。

Python スクリプティング

USD で Python スクリプトを活用すると、ユーザー インターフェースを使うと困難で時間のかかるシーンの編集やバッチ処理など、さまざまな操作を柔軟に実行できるようになります。柔軟性の高い Python スクリプトを [Output Log (アウトプット ログ)] パネルから実行することで、多数のプリミティブの属性を非表示にしたり、編集したりといった操作をすばやく自動化できます。

Python スクリプトを Unreal Engine で使用するには、まず Python Editor Script プラグイン を有効にする必要があります。詳細は「プラグインを操作する」を参照してください。

そして、レベルエディタ下部にあるアウトプット ログを使用して、Python スクリプトを実行することができます。また、[Window] > [Output Log] から、[Output Log] をパネルとして開くこともできます。

画像をクリックして拡大表示します。

Unreal Engine で Python スクリプトを使用する詳細については、「Python を使用した Unreal Editor のスクリプティング」を参照してください。

Python スクリプトのユースケース

Unreal Engine とともにシッピングされる USD SDK バージョンが「21.05」にアップグレードされた際に、USDLux Light スキーマ内のいくつかの属性の名前が変更されました。問題を最小限に抑えるために、Unreal Engine は、USDLux プリミティブ属性の名前を 21.05 バージョンの命名規則に従って変更する Python スクリプトとともにシッピングされます。

    from pxr import Usd, Sdf, UsdLux
    import argparse

    def rename_spec(layer, edit, prim_path, attribute, prefix_before, prefix_after):
        path_before = prim_path.AppendProperty(prefix_before + attribute)
        path_after = prim_path.AppendProperty(prefix_after + attribute)

        # We must check every time, because adding a namespace edit that can't be applied will just cancel the whole batch
        if layer.GetAttributeAtPath(path_before):
            print(f"Trying to rename '{path_before}' to '{path_after}'")
            edit.Add(path_before, path_after)

    def rename_specs(layer, edit, prim_path, reverse=False):
        prefix_before = 'inputs:' if reverse else ''
        prefix_after = '' if reverse else 'inputs:'

        # Light
        rename_spec(layer, edit, prim_path, 'intensity', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'exposure', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'diffuse', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'specular', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'normalize', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'color', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'enableColorTemperature', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'colorTemperature', prefix_before, prefix_after)

        # ShapingAPI
        rename_spec(layer, edit, prim_path, 'shaping:focus', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'shaping:focusTint', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'shaping:cone:angle', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'shaping:cone:softness', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'shaping:ies:file', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'shaping:ies:angleScale', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'shaping:ies:normalize', prefix_before, prefix_after)

        # ShadowAPI
        rename_spec(layer, edit, prim_path, 'shadow:enable', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'shadow:color', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'shadow:distance', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'shadow:falloff', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'shadow:falloffGamma', prefix_before, prefix_after)

        # DistantLight
        rename_spec(layer, edit, prim_path, 'angle', prefix_before, prefix_after)

        # DiskLight, SphereLight, CylinderLight
        # Note: treatAsPoint should not have the 'inputs:' prefix so we ignore it
        rename_spec(layer, edit, prim_path, 'radius', prefix_before, prefix_after)

        # RectLight
        rename_spec(layer, edit, prim_path, 'width', prefix_before, prefix_after)
        rename_spec(layer, edit, prim_path, 'height', prefix_before, prefix_after)

        # CylinderLight
        rename_spec(layer, edit, prim_path, 'length', prefix_before, prefix_after)

        # RectLight, DomeLight
        rename_spec(layer, edit, prim_path, 'texture:file', prefix_before, prefix_after)

        # DomeLight
        rename_spec(layer, edit, prim_path, 'texture:format', prefix_before, prefix_after)

    def collect_light_prims(prim_path, prim, traverse_variants, light_prim_paths, visited_paths):
        if not prim:
            return

        if prim_path in visited_paths:
            return
        visited_paths.add(prim_path)

        # Traverse manually because we may flip between variants, which would invalidate the stage.Traverse() iterator
        for child in prim.GetChildren():

            # e.g. /Root/Prim/Child
            child_path = prim_path.AppendChild(child.GetName())

            if UsdLux.Light(child):
                light_prim_paths.add(child_path)

            traversed_grandchildren = False
            if traverse_variants:
                varsets = child.GetVariantSets()
                for varset_name in varsets.GetNames():
                    varset = varsets.GetVariantSet(varset_name)
                    original_selection = varset.GetVariantSelection() if varset.HasAuthoredVariantSelection() else None

                    # Switch selections only on the session layer
                    with Usd.EditContext(prim.GetStage(), prim.GetStage().GetSessionLayer()):
                        for variant_name in varset.GetVariantNames():
                            varset.SetVariantSelection(variant_name)

                            # e.g. /Root/Prim/Child{VarName=Var}
                            varchild_path = child_path.AppendVariantSelection(varset_name, variant_name)

                            collect_light_prims(varchild_path, child, traverse_variants, light_prim_paths, visited_paths)
                            traversed_grandchildren = True

                            if original_selection:
                                varset.SetVariantSelection(original_selection)
                            else:
                                varset.ClearVariantSelection()

            if not traversed_grandchildren:
                collect_light_prims(child_path, child, traverse_variants, light_prim_paths, visited_paths)

    def update_lights_on_stage(stage_root, traverse_variants=False, reverse=False):
    """ Traverses the stage with root layer `stage_root`, updating attributes of light prims to/from USD 21.05.

        The approach here involves traversing the composed stage and collecting paths to prims that are UsdLux lights
        (flipping through variants or not according to the input argument), and later on traverse all the stage's
        layers and renaming all specs of of light prim attributes to 21.05 (by adding the 'inputs:' prefix) or to
        the schema before 21.05 (by removing the 'inputs:' prefix).

        We traverse the composed stage first to make sure we're modifying exclusively UsdLux light prim attributes,
        avoiding modifications to a Sphere's "radius" attribute, for example.
        """
        stage = Usd.Stage.Open(stage_root, Usd.Stage.LoadAll)
        layers_to_traverse = stage.GetUsedLayers(True)

        # Collect UsdLux prims on the composed stage
        light_prim_paths = set()
        visited_paths = set()
        collect_light_prims(Sdf.Path("/"), stage.GetPseudoRoot(), traverse_variants, light_prim_paths, visited_paths)

        print("Collected light prims:")
        for l in light_prim_paths:
            print(f"\t{l}")

        # Traverse all layers, and rename all relevant attributes of light prims
        visited_paths = set()
        for layer in layers_to_traverse:
            # Batch all rename operations for this layer in a single namespace edit
            edit = Sdf.BatchNamespaceEdit()

            def visit_path(path):
                attr_spec = layer.GetAttributeAtPath(path)
                if attr_spec:
                    prim_path = attr_spec.owner.path

                    # Only visit each prim once, as we'll handle all UsdLux properties in one go
                    if prim_path in visited_paths:
                        return
                    visited_paths.add(prim_path)

                    if prim_path in light_prim_paths:
                        rename_specs(layer, edit, prim_path, reverse)

            layer.Traverse("/", visit_path)

            if len(edit.edits) == 0:
                print(f"Nothing to rename on layer '{layer.identifier}'")
            else:
                if layer.CanApply(edit):
                    layer.Apply(edit)
                    print(f"Applied change to layer '{layer.identifier}'")
                else:
                    print(f"Failed to apply change to layer '{layer.identifier}'")

        # Save all layers
        for layer in layers_to_traverse:
            if not layer.anonymous:
                layer.Save()

    if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Update light prims to USD 21.05')
        parser.add_argument('root_layer_path', type=str,
                        help='Full path to the root layer of the stage to update e.g. "C:/MyFolder/MyLevel.usda"')
        parser.add_argument('--v', '--traverse_variants', dest='traverse_variants', action='store_true',
                        help='Whether to also flip through every variant in every variant set encountered when looking for light prims')
        parser.add_argument('--r', '--reverse', dest='reverse', action='store_true',
                        help='Optional argument to do the reverse change instead: Rename 21.05 UsdLux light attributes so that they follow the schema from before 21.05')
        args = parser.parse_args()

        update_lights_on_stage(args.root_layer_path, args.traverse_variants, args.reverse)

このスクリプトは、「Engine/Plugins/Importers/USDImporter/Content/Python/usd_unreal/update_lights_to_21_05.py」にある USDImporter ソース ファイルに含まれています。

このスクリプトを [Output Log] から実行するには、次の手順に従ってください。

  1. [Window] > [Developer Tools] > [Output Log] を選択して [Output Log] ウィンドウを開きます。

  2. コマンドライン フィールドの左側にある [Cmd] ドロップダウンをクリックして [Python] を選択します。

  3. コマンドライン フィールドに次のコマンドを入力します。 "C:\Program Files\Epic Games\UE_4.27\Engine\Plugins\Importers\USDImporter\Content\Python\usd_unreal\update_lights_to_21_05.py" "C:/path/to/root_layer.usda"

    "C:/path/to/root_layer.usda"」の部分は USD ファイルへのパスです。

    上記のサンプルには、Unreal Engine インストール ディレクトリへのデフォルトのパスが含まれています。使用しているバージョンの Unreal Engine をデフォルトの場所にインストールしていない場合は、パスを適宜調整してください。

  4. Enter キーを押してコマンドを実行します。

Unreal Engine のドキュメントを改善するために協力をお願いします!どのような改善を望んでいるかご意見をお聞かせください。
調査に参加する
キャンセル