USDのUsdPreviewSurfaceとOmniverseのMDL(OmniPBR/OmniGlass)

  • by

USDの標準のマテリアルのShaderは「UsdPreviewSurface」と呼ばれるものを指定します。
前回、USDのUsdPreviewSurfaceは非破壊とは少し違う的なことを書いたのを補足しておきます。
USDでのPBRマテリアルについてまとめることにしました。

Omniverse Create 2021.1.1で検証しています。

PBRマテリアル

昨今のリアルタイムエンジンは、マテリアルは「PBRマテリアル」で指定することがほとんどだと思われます。
オフラインレンダラも最近のものはPBRマテリアルを使用することが多いです。
現状、このPBRマテリアルを保持できるオープンなファイルフォーマットでよく使われるものは、自分の知る限りはglTFかUSDのどちらかになると思います。

マテリアルとShader

マテリアルのパラメータ(BaseColor/Roughness/Metallicなど)は、「Shader」によって記載されます。
DCCツールには独自のパラメータがある場合もあり、これらを表現するためにShaderを使ってマテリアルを拡張することができます。
リアルタイムエンジンの場合は、このShaderはGPU内でShader言語を与えて処理されます。
USDの標準は「UsdPreviewSurface」がShaderとして割り当てられますが、別のものを指定することも可能です。
ただし、それはUSDの仕様範囲外になります。
例えば、NVIDIA社のMDL(Material Definition Language)をUSDのマテリアルのShaderとして割り当てることができます。
これは、USD側ではなくNVIDIA社が定義した仕様となります。

Omniverse(Omniverse Kitを使用したアプリやコネクタ)を扱う場合は、MDLを使用するほうが整合性が取りやすいかもしれません。
今回書いていく内容はこのあたりの話になります。

OmniverseでマテリアルのShaderを新規割り当て

Omniverse CreateではStageウィンドウで対象形状を右クリックし、ポップアップメニューの「Create」-「Materials」を選択すると、マテリアルで使用するShaderを指定できます。

これは、形状に対して新規のマテリアルを割り当てる操作になります。
形状に割り当てられているマテリアルは、PropertyウィンドウのMaterials on selected modelsで確認できます。

以下のShaderの種類があります。

Shader名 内容
OmniGlass ガラス表現
OmniPBR PBRマテリアル(透明+屈折を使わない表現)
OmniPBRClearCoat OmniPBRにClearCoatが追加された表現
USDPreviewSurface USDの標準Shader

その他「xxx Opacity」名のShaderがありますが、これは現在は使われません。
Opacityについては上記のShaderにはすでに含まれています。
OmniXXX名のものは、MDLを使用したマテリアルを表現するShaderになります。

UsdPreviewSurfaceとMDL(OmniPBR/OmniGlass)

Omniverseでは、USDのマテリアルとしてUsdPreviewSurfaceを使用するよりもMDL(OmniPBR/OmniGlass)を使うほうが非破壊の状態を保つことができ、またUSDの構造もシンプルに扱えます。

UsdPreviewSurfaceが複雑になる構造

USDのインポータ/エクスポータで対処されるため特に意識する必要がない部分ではありますが、
以下の場合にUSDのマテリアルの構造が複雑化します。

  • UsdPreviewSurfaceでテクスチャを扱う場合
  • テクスチャの繰り返しなどを指定する場合(UsdTransform2d)

以下のような球にテクスチャをマッピングする場合を考えます。

UsdPreviewSurfaceでテクスチャを扱う場合、テキストファイルのusdaでは以下のような記述になります。

def Material "material"
{
    token inputs:frame:stPrimvarName = "st"
    token outputs:surface.connect = </root/Materials/material/PBRShader.outputs:surface>

    def Shader "PBRShader"
    {
        uniform token info:id = "UsdPreviewSurface"
        color3f inputs:diffuseColor.connect = </root/Materials/material/diffuseTexture.outputs:rgb>
        float inputs:ior = 1.5
        float inputs:metallic = 0
        float inputs:opacity = 1
        float inputs:roughness = 0.2
        token outputs:surface
    }

    def Shader "stReader"
    {
        uniform token info:id = "UsdPrimvarReader_float2"
        token inputs:varname.connect = </root/Materials/material.inputs:frame:stPrimvarName>
        float2 outputs:result
    }

    def Shader "diffuseTexture"
    {
        uniform token info:id = "UsdUVTexture"
        asset inputs:file = @material_albedo.jpg@
        float2 inputs:st.connect = </root/Materials/material/stReader.outputs:result>
        token inputs:wrapS = "repeat"
        token inputs:wrapT = "repeat"
        color3f outputs:rgb
    }
}

これは、"/root/Materials"に"materials"のマテリアルを作成しています。
構造にすると以下のような流れになります。これは、Omniverse CreateのStageウィンドウでの表示です。

Materialから"PBRShader"名のShaderに接続します。USDでは「connect」として接続を指定します。
"PBRShader"では使用するShaderの種類"UsdPreviewSurface"やdiffuseColor/ior/metallic/opacity/roughness/normalなどを指定しています。
テクスチャを割り当てる場合、diffuseColorから"diffuseTexture"名の"UsdUVTexture"に接続します。
テクスチャを割り当てる場合はUVの指定が必要なため、さらに"stReader"名の"UsdPrimvarReader_float2"の指定に接続します。
ここから、material.inputs:frame:stPrimvarNameに接続します。
ここでは"st"と指定されており、これはジオメトリのUV情報につながります。

このように、connectで繋げていく指定が必要です。
この接続指定は、diffuseColorだけでなくmetallic/roughness/opacity/normalなどのテクスチャ指定が可能な要素でそれぞれ必要となります。

テクスチャの繰り返しなどを指定する場合はさらに複雑になります。
以下は、テクスチャのスケール(2, 1)を指定しています。

def Material "material"
{
    token inputs:frame:stPrimvarName = "st"
    token outputs:surface.connect = </root/Materials/material/PBRShader.outputs:surface>

    def Shader "PBRShader"
    {
        uniform token info:id = "UsdPreviewSurface"
        color3f inputs:diffuseColor.connect = </root/Materials/material/diffuseTexture.outputs:rgb>
        float inputs:ior = 1.5
        float inputs:metallic = 0
        float inputs:opacity = 1
        float inputs:roughness = 0.2
        token outputs:surface
    }

    def Shader "stReader"
    {
        uniform token info:id = "UsdPrimvarReader_float2"
        token inputs:varname.connect = </root/Materials/material.inputs:frame:stPrimvarName>
        token outputs:result
    }

    def Shader "diffuseTexture_Transform2d"
    {
        uniform token info:id = "UsdTransform2d"
        token inputs:in.connect = </root/Materials/material/stReader.outputs:result>
        float inputs:rotation = 0
        float2 inputs:scale = (2, 1)
        float2 inputs:translation = (0, 0)
        float2 outputs:result
    }

    def Shader "diffuseTexture"
    {
        uniform token info:id = "UsdUVTexture"
        asset inputs:file = @material_albedo.jpg@
        float2 inputs:st.connect = </root/Materials/material/diffuseTexture_Transform2d.outputs:result>
        token inputs:wrapS = "repeat"
        token inputs:wrapT = "repeat"
        color3f outputs:rgb
    }
}

※ Omniverseでは、UsdPreviewSurfaceで「UsdTransform2d」を使用する記述に対応していないようです。

順番に接続をたどっていきます。
Materialから"PBRShader"名のShaderに接続します。
テクスチャを割り当てる場合、"PBRShader"のdiffuseColorから"diffuseTexture"名の"UsdUVTexture"に接続します。
"diffuseTexture"から"diffuseTexture_Transform2d"名の"UsdTransform2d"に接続します。
これがテクスチャ変換のために追加された要素です。
ここで、テクスチャ空間上の回転/拡大縮小/移動を指定します。
テクスチャを割り当てる場合はUVの指定が必要なため、さらに"stReader"名の"UsdPrimvarReader_float2"の指定に接続します。
ここから、material.inputs:frame:stPrimvarNameに接続します。
ここでは"st"と指定されており、これはジオメトリのUV情報につながります。

このように、connectでの接続により複雑になっています。
また、この記述により非破壊ではない部分も出てきます。

UsdPreviewSurfaceでベイクが発生する構成

「inputs:diffuseColor」の箇所で、PBRマテリアルの「BaseColor」の色を指定することになります。
テクスチャを用いない単色指定の場合は以下のように記述します。

    def Shader "PBRShader"
    {
        uniform token info:id = "UsdPreviewSurface"
        color3f inputs:diffuseColor = (0.5, 0.4, 0.6)
        float inputs:ior = 1.5
        float inputs:metallic = 0
        float inputs:opacity = 1
        float inputs:roughness = 0.2
        token outputs:surface
    }

inputs:diffuseColorの色(RGB)として(0.5, 0.4, 0.6)と指定しています。
diffuseColorとしてテクスチャを指定する場合は「inputs:diffuseColor」を「inputs:diffuseColor.connect」に置き換えて"UsdUVTexture"への接続を指定することになります。

    def Shader "PBRShader"
    {
        uniform token info:id = "UsdPreviewSurface"
        color3f inputs:diffuseColor.connect = </root/Materials/material/diffuseTexture.outputs:rgb>
        float inputs:ior = 1.5
        float inputs:metallic = 0
        float inputs:opacity = 1
        float inputs:roughness = 0.2
        token outputs:surface
    }

    def Shader "diffuseTexture"
    {
        uniform token info:id = "UsdUVTexture"
        asset inputs:file = @material_albedo.jpg@
        float2 inputs:st.connect = </root/Materials/material/stReader.outputs:result>
        token inputs:wrapS = "repeat"
        token inputs:wrapT = "repeat"
        color3f outputs:rgb
    }

では、この場合の「diffuseColor」の単色はどこにいったかというと、この構造だと指定できません。
「テクスチャを扱う場合は、テクスチャに対して要素を全部ベイクする必要がある」ということになります。
glTFフォーマットや多くのDCCツールの場合、BaseColor色とBaseColorTextureは分かれており、レンダリング時にはそれを乗算合成したものを採用するというのが多いです。
ですが、USDのUsdPreviewSurfaceで同一の表現を行う場合は、
テクスチャの各ピクセルに対してBaseColor色を乗算したイメージをファイルで用意する必要があります。

これらを解決するためにMDLを使用します。
OmniPBR/OmniGlassは、MDLのあらかじめ用意されたプリセットになります。

MDLのOmniPBRに置き換える

OmniPBRを使用してBaseColorにテクスチャを指定し、テクスチャの繰り返しを 2 x 1としました。

usdaは以下のようになります。
なお、以下はデフォルト値の指定(も可能です)を省略しています。

def Material "material"
{
    token outputs:mdl:surface.connect = </root/Materials/material/Shader.outputs:out>

    def Shader "Shader"
    {
        uniform token info:implementationSource = "sourceAsset"
        uniform asset info:mdl:sourceAsset = @OmniPBR.mdl@
        uniform token info:mdl:sourceAsset:subIdentifier = "OmniPBR"
        asset inputs:diffuse_texture = @material_albedo.jpg@
        color3f inputs:diffuse_tint = (1, 1, 1)
        float inputs:metallic_constant = 0
        float inputs:reflection_roughness_constant = 0.2
        float2 inputs:texture_scale = (2, 1)
        token outputs:out
    }
}

マテリアルのconnect構造は、MaterialからOmniPBRのShaderを参照しているだけです。

構造がシンプルになっています。

また、「inputs:diffuse_texture」のBaseColorテクスチャと「inputs:diffuse_tint」のBaseColor色は乗算合成されて表現されることになります。
「inputs:texture_scale」を(2, 1)とすることで、テクスチャの繰り返しの数を2 x 1にしています。

これにより、UsdPreviewSurfaceで問題に上げた複雑さと非破壊でなくなる件が解決します。
ここでは「OmniPBR.mdl」というMDLファイルが「OmniPBR」としてShader指定されていることになります。
マテリアルのパラメータが増える場合はこのMDLファイルを環境に合わせて与える、ということもできそうです(まだ未検証です)。

OmniPBR/OmniGlass表現については後述します。

USDのPBRマテリアル

以降は、PBRマテリアルの表現について記載します。
PBRマテリアルは、BaseColor/Metalloc/Roughnessを基本としたMetallicワークフロー(glTFでのMetallic-Roughness Material)となり、オプションとしてEmissive/Normal/Occlusionを追加できる構成になります。
USD(UsdPreviewSurface)では別途、Specularワークフローという映り込みで色を指定するモードもあります。

PBRマテリアルと定義している場合、レンダラにより多少の表現の差はありますが同じパラメータで同じ表現になります。

以下はBaseColorを赤色にして、Roughnessを0.0/0.2/0.4/0.6と変化させたときの例です。
Intel社のOSPRayでは以下のようになりました。

Omniverse Createでは以下のようになりました。

光源や背景のIBL(DomeLightで指定)は、後から追加しています。

上記にMetallic 1.0を与えました。
※ 一般的に、Metallicは0.0か1.0のいずれかを指定します。
Metallic 1.0の場合は金属的な表現となります。

Intel社のOSPRayでは以下のようになりました。

Omniverse Createでは以下のようになりました。

Metallicを使うと背景の映り込みが正しくならない場合がある ?

Omniverse Createでは映り込み部分で、窓からのまぶしい光で白くなる部分/ハイライトが反映されていません。

これは、BaseColorのGreen/Blue成分が0になっているのが原因のようです(UsdPreviewSurface/OmniPBRどちらも同じ現象が起きました)。

Base ColorのRGB成分は0.0にならないようにします。
最小値を0.01にしました。

以下のように映り込みに白が乗りました。

PBRの物理ベースとして考えると、完全な白や完全な黒はこの世に存在しません。
RGB成分で分ける場合も同様で、それぞれの成分が0.0または1.0になることはないという前提で進めたほうがよいかもしれません。
RGBそれぞれで0.001 ~ 0.999みたいな範囲で指定。
0.0や1.0を指定すると上記のようになる、というのはPBRの挙動としては正しくて仕様なのかもという気もします。

その他、表現できることを列挙していきます。

法線マップの使用

現状のOmniverse CreateではマテリアルでUsdPreviewSurfaceとして法線マップを使用すると、正しく表現できません。

https://forums.developer.nvidia.com/t/seams-when-using-normal-maps-in-omniverse-create/174823

これは、法線マップ使用時にColor Spaceで「raw」を指定する必要があるとのことでした。
これをMDL(OmniPBR)に置き換えると以下のように正しく表現されます。


UsdPreviewSurface時は、これの回避手段は今のところないようです。

Eimssiveでの発光

UsdPreviewSurfaceでは、発光としてEmissiveColorを指定できます。
これを発光色としていますが、これはリアルタイムにおける加算合成的な扱いになるようです。
周囲を照らす光源になるわけではありません(EmissiveColorの色だけのデータとなり、光の強さのパラメータがありません)。

OmniPBRを使用することで、「Emissive Color」に加えて「Emissive Intensity」で発光としての強さを指定できます。
以下は、マテリアル(OmniPBR)としてのEmissiveだけで照らしたシーンです。


現状は光の強さの単位が何かは自分自身解析できてません。
表面積に依存しないことからLumenに比例するものと考えてますが、これは単位を突き止めたいところです。

OpacityとTransparency(もしくはTransmission)

Opacity(不透明)とTransparency(透明)は真逆の関係になります。
Opacityは0.0に近づくほど完全透過、1.0で不透過になります。
Transparencyは0.0に近づくほど不透過、1.0に近づくほど透明になります。

かつてリアルタイムでは、半透明形状がある場合の「屈折」は表現できませんでした。
できたとしてもフェイクな表現かスクリーンスペースの処理(エフェクトとしての実装)となります。
透過する表現をリアルタイムで行う場合、不透明の面を先に描画した後、
透過する面を「スクリーンの奥から」描画する必要がありました。
そのため、交差する面の前後関係により不正な描画が出たり、2パス描画の手間がかかっていました。
といっても、それらはリアルタイムエンジンが処理するため意識する必要はありません。

「レイトレーシング」となった段階で、「屈折」や「透明」がオフラインレンダラの手順でそのまま使用できるようになり、めんどうなリアルタイムの制限からも解放されます。
今まではOpacityだけで事足りていたことも、屈折が入るとパラメータとして不足することになります。
そこで、透明度(Transparency)のパラメータが出てきます。
(OpacityとTransparencyは別々で扱う存在で、まとめてはいけません)
これは環境によってはTransmissionと命名されていることもあります。
PBRマテリアルの透明では、厚みや減衰距離の指定があるものも多いです。
これにより物体内部での光の散乱が起きることになり、人肌などのより柔らかいマテリアルの表現ができるようになります。
現状のOmniverseのMDL(OmniPBR/OmniGlass)ではその表現はまだ実装されていないようです。

以下はOmniverse Createの「RTX Path-traced」のレンダリングです。

左がOmniPBR、右がOmniGlassを割り当てました。
OmniPBRではIOR(屈折率)の指定はありません。金属表現でもIORは使用されるため、OmniPBRではIORとして1.5が固定で割り当てられていると思われます。
OmniPBRの場合はOpacityを指定することができます。しかし、これはTransparencyの指定ではありません。
Opacityだけでは透過+屈折は表現できません。UsdPreviewSurfaceの場合も、OpacityはありますがTransparencyのパラメータはありません。
なお、OmniPBR/OmniGlassのどちらにもOpacityは存在し、「Opacity」の「Enable Opacity」チェックボックスをOnにして「Opacity Amount」でOpacity値を指定します。

このOpacityによる半透明表現を「RTX Real-time」で効かせるには、「RTX Settings」の「Ray Tracing」で「Translucency」「Enable Fractional Cutout Opacity」チェックボックスをOnにする必要があるようでした。

OmniGlass

ガラスのような透過+屈折がある場合は、OmniGlassを使用することになります。
ここでは透明度のパラメータはありませんでした。
グラス色やRoughness、反射色(Reflection Color)の指定は可能です。
以下は、Roughness 0.0と0.2を指定したものです。

OmniGlassのThin

「Thin Walled」チェックボックスをOnにすると、その形状は中身が詰まっておらず薄膜があるものとしてレンダリングされます。
なお、薄膜の厚みに相当するパラメータはありませんでした。
以下は左が「Thin Walled」Off、右がOnにしたものです。

左は透明形状で中身が詰まった感じ、右は薄い膜で覆われた表現になります。
薄膜の形状は、ポリゴンメッシュの面だけで構成されるようなジオメトリ(中身は存在しない)を想像すると分かりやすいかもしれません。
Thinは、以下のような電球のガラス表現で有効です。

DoubleSided

USDでは両面を描画する指定は、ジオメトリ側で指定します。
※ このDoubleSided指定は、多くのフォーマットではマテリアルに指定する場合が多いです。

「RTX Settings」の「Common」の「Back Face Culling」をOnにし、
Meshの「Single Sided」をOnにすると、片面描画になります。

以下は、「RTX Settings」の指定です。

以下は、Meshの「Visual」-「Single Sided」の指定です。

以下は、左と右の緑の形状は3つの面で構成される同じ形です。
左側をSingle Sidedの指定にしました。

影はSingle Sidedの影響を受けていないようでした。
ジオメトリの「Cast Shadows」チェックボックスをOffにすることで、影を無効化することはできます。

USDではリアルタイムの表現に従い、デフォルトではDouble Sided はOff、必要に応じてDouble SidedをOnにする、という使い方になります。
Omniverseでは逆で、デフォルトでDouble SidedがOnになる感じです。

あと、USDの標準では「singleSided」というパラメータは存在しません。Omniverseの拡張になるのかもしれません。
doubleSidedは標準で存在します。
このあたりはまだ仕様が確定していないのかもしれません。

マテリアルについてはまだまだいろいろTipsはあるのですが、今回はここまでです。
USDのUsdPreviewSurfaceと比べてMDLが優位な点、それぞれの表現の特徴の説明でした。
また情報がありましたらブログに書いていくようにします。