USDの実践メモ 04 : テクスチャマッピング


[前回](https://qiita.com/snowxcrash/items/7ab2659cb895ca3a2e00)に続き，今回はテクスチャマッピングを行います。用いる画像は USD のロゴマークとします。

![USDLogoLrg_180x176.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659949/c080235c-3f3f-cba6-2fbb-b7d332023582.png)

## テクスチャマッピング

### テクスチャリーダーとプリミティブ変数リーダー

前回は PBR シェーダーの *diffuseColor* や *opacity* に特定の色（マゼンダ）や特性を割り当てましたが，今回はここにテクスチャを割り当てます。そのために，テクスチャリーダー（UsdUVTexture）とプリミティブ変数リーダー（UsdPrimvarReader_float2）の 2 つのシェーダーノードを用意します（これらについては公式の[仕様書](https://openusd.org/release/spec_usdpreviewsurface.html)に詳しい説明があります）。

まずは，テクスチャリーダーから。*file* アトリビュートにテクスチャ（画像ファイル）のパスを指定します。マッピングに使用する *st* 座標 はマテリアルに張るメッシュを参照して得るようにするので，ここでは初期化しません。出力は *rgb* と *a* で，それぞれこのあとのプログラムでシェーダーの *diffuseColor* と *opacity* に接続されます。

```Python
def create_uvtexture(stage, path):
    uvtexture = UsdShade.Shader.Define(stage, path)
    uvtexture.CreateIdAttr('UsdUVTexture')

    # Inputs
    uvtexture.CreateInput('file', Sdf.ValueTypeNames.Asset).Set('./USDLogoLrg.png')
    uvtexture.CreateInput('st', Sdf.ValueTypeNames.Float2)

    # Outputs
    uvtexture.CreateOutput('rgb', Sdf.ValueTypeNames.Float3)
    uvtexture.CreateOutput('a', Sdf.ValueTypeNames.Float)

    return uvtexture
```

次に，プリミティブ変数リーダーを用意します。これはメッシュから st 座標を取り込むなど，ジオメトリプリムから情報を取得するときに使うものです。今回は st 座標の取得にしか使いませんので，2次元配列を返す float_2 型のリーダーだけを準備しておけば充分です。

```Python
def create_primvarReader(stage, path):
    primvarReader = UsdShade.Shader.Define(stage, path)
    primvarReader.CreateIdAttr('UsdPrimvarReader_float2')

    # Inputs
    primvarReader.CreateInput('varname', Sdf.ValueTypeNames.String)

    # Outputs
    primvarReader.CreateOutput('result', Sdf.ValueTypeNames.Float2)

    return primvarReader
```

:::note
varname のデータ型は Token ではなく String にします。コロンなどの特殊文字の対応のためだと思われます。たしかに[ここ](https://openusd.org/release/spec_usdpreviewsurface.html#primvar-reader)のプリミティブ変数リーダーの定義でも string になっています。誤って Token にしてしまった場合は usdcheker に掛けると間違いを指摘してくれます。
:::

## メッシュノードの修正

前々回作成したメッシュノードを修正して，UV 座標データを与えます。一例として，下図のように左右の面で異なる向きの座標を与えてみます（右は90度回転している）。

![uvcoord_480x360.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659949/f32e5b96-f204-7e4d-3af3-d6b927b0b930.png)

```Python
def create_mesh(stage, path):
    mesh = UsdGeom.Mesh.Define(stage, path)

    # Points, FaceVertexCounts, FaceVertexIndices は前回と同じ
    ...

    # UV
    uvList = []
    uvList.append((0, 0))
    uvList.append((1, 0))
    uvList.append((1, 1))
    uvList.append((0, 1))
    uvList.append((0, 0))
    uvList.append((1, 0))
    uvList.append((1, 1))
    uvList.append((0, 1))

    primvar_api = UsdGeom.PrimvarsAPI(mesh)
    primvar = primvar_api.CreatePrimvar('st', Sdf.ValueTypeNames.TexCoord2fArray, interpolation=UsdGeom.Tokens.faceVarying)
    primvar.GetAttr().Set(uvList)

    uvIndexList = []
    uvIndexList.extend((0, 1, 2, 3))
    uvIndexList.extend((5, 6, 7, 4))

    primvar.SetIndices(uvIndexList)

    # DoubleSided も前回と同じ
    ...

    return mesh
```

## マテリアルノードの修正

ここでいま一度，シェーダー，マテリアル，メッシュの各々が保持する surface 情報を整理してみます。色（diffuseColor）と 透明度（opacity）はシェーダーの input アトリビュートに定義されますが，データの中身はテクスチャリーダーによって外から得ます。そのテクスチャリーダーは，input アトリビュートの定義に見られるように，UV（st）を必要とします。しかしながらそのデータは，いま，メッシュが保持しています。従って，シェーダーネットワークのコンテナであるマテリアルとメッシュとの接続が必要になってきます。そこでマテリアルノードに，メッシュから st 情報を取得するための接続ターミナルを用意します。

```Python
def create_material(stage, path):
    material = UsdShade.Material.Define(stage, path)

    # Inputs
    material.CreateInput('frame:stPrimvarName', Sdf.ValueTypeNames.Token).Set('st')

    # Outputs は前回と同じ
    ...

    return material
```

## 接続

シェーダー，マテリアル，メッシュを接続します。本件ではメインプログラムでこれを行うことにします（ステージ作成からシェーダー作成までは前回までの記事と同じです）。

```Python
if __name__ == '__main__':
    # ステージ作成
    stage = create_stage()

    # ルートノード作成
    rootNode = create_node(stage, '/World', is_defaultPrim=True)

    # メッシュ作成
    mesh = create_mesh(stage, rootNode.GetPath().AppendChild('Mesh'))

    # マテリアル作成
    material = create_material(stage, rootNode.GetPath().AppendChild('Material'))

    # シェーダー作成
    shader = create_shader(stage, material.GetPath().AppendChild('PBRShader'))

    # プリミティブ変数リーダー作成
    stReader = create_primvarReader(stage, material.GetPath().AppendChild('StReader'))

    # テクスチャリーダー作成
    uvTextureReader = create_uvtexture(stage, material.GetPath().AppendChild('Texture'))

    # 接続 (1): マテリアルの output ターミナルをシェーダーに接続
    material.GetSurfaceOutput().ConnectToSource(shader.ConnectableAPI(), 'surface')

    # 接続 (2): シェーダーの input ターミナルをテクスチャリーダーの output ターミナルに接続
    shader.GetInput('diffuseColor').ConnectToSource(uvTextureReader.ConnectableAPI(), 'rgb')
    shader.GetInput('opacity').ConnectToSource(uvTextureReader.ConnectableAPI(), 'a')

    # 接続 (3): テクスチャリーダーの input ターミナルをプリミティブ変数リーダーの output ターミナルに接続
    uvTextureReader.GetInput('st').ConnectToSource(stReader.ConnectableAPI(), 'result')

    # 接続 (4): プリミティブ変数リーダーの input ターミナルをマテリアルに接続
    stReader.GetInput('varname').ConnectToSource(material.GetInput('frame:stPrimvarName'))

    # メッシュとマテリアルのバインド
    api = UsdShade.MaterialBindingAPI(mesh)
    api.Apply(mesh.GetPrim()).Bind(material)

    # USDファイルにエクスポート
    stage.GetRootLayer().Export('./Material.usda')
```

出力は次のとおりになりました。usdchecker もパスしています。

```bash
#usda 1.0
(
    defaultPrim = "World"
    metersPerUnit = 0.01
    upAxis = "Y"
)

def Xform "World"
{
    def Mesh "Mesh" (
        prepend apiSchemas = ["MaterialBindingAPI"]
    )
    {
        uniform bool doubleSided = 0
        int[] faceVertexCounts = [4, 4]
        int[] faceVertexIndices = [0, 1, 4, 3, 1, 2, 5, 4]
        rel material:binding = </World/Material>
        point3f[] points = [(-1, 0, 0), (0, 0, 0), (1, 0, 0), (-1, 1, 0), (0, 1, 0), (1, 1, 0)]
        texCoord2f[] primvars:st = [(0, 0), (1, 0), (1, 1), (0, 1), (0, 0), (1, 0), (1, 1), (0, 1)] (
            interpolation = "faceVarying"
        )
        int[] primvars:st:indices = [0, 1, 2, 3, 5, 6, 7, 4]
    }

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

        def Shader "PBRShader"
        {
            uniform token info:id = "UsdPreviewSurface"
            color3f inputs:diffuseColor.connect = </World/Material/Texture.outputs:rgb>
            float inputs:metallic = 0
            float inputs:opacity.connect = </World/Material/Texture.outputs:a>
            float inputs:roughness = 0.4
            token outputs:surface
        }

        def Shader "StReader"
        {
            uniform token info:id = "UsdPrimvarReader_float2"
            string inputs:varname.connect = </World/Material.inputs:frame:stPrimvarName>
            float2 outputs:result
        }

        def Shader "Texture"
        {
            uniform token info:id = "UsdUVTexture"
            asset inputs:file = @./USDLogoLrg_340x332.png@
            float2 inputs:st.connect = </World/Material/StReader.outputs:result>
            float outputs:a
            float3 outputs:rgb
        }
    }
}
```

```bash
$ usdchecker Material.usda 
Success!
```

マテリアルを表示してみます。左右ふたつの面にそれぞれロゴがマッピングされました（右側は90度回転している）。

![material_logo_395x200.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3659949/9ee1813d-f2d9-bbd4-3469-5078cd7ab11c.png)

## まとめ

テクスチャの情報をPBRシェーダのパラメータに繋ぎながら，マテリアルへのマッピングを実現しました。

## 参考資料

ft-lab (Yutaka Yoshisaka) さん，あんどうめぐみさん，OpenUSD公式 による以下のサイトが理解の助けになりました。

https://github.com/ft-lab/Documents_USD/blob/master/doc/usd_create_material.md

https://fereria.github.io/reincarnation_tech_bk/65_SampleCode/Notebook/USD/Material/USDMaterial_01/

https://openusd.org/release/spec_usdpreviewsurface.html

https://openusd.org/release/tut_simple_shading.html