カスタムノードをUSDの世界に取り込んでみよう(マテリアル編)

執筆中

## はじめに
前回(OBJ編)に引き続き、今回はマテリアルをUSDの世界に取り込んでみましょう。


## マテリアルをUSDへ変換
シーンはScene Import
が提供する仕組みを利用してトランスレータープラグインをpythonで実装できましたが、マテリアルも同様にpython(C++も)で変換できる仕組みが用意されています。概要は以下のドキュメントに記載されています。
https://www.sidefx.com/docs/houdini/solaris/shader_framework.html
トランスレーターの実装は以下にざっくりとした説明が記載されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144656/c00c14a1-a267-06e9-8fdb-310790e7fa9f.png)

マテリアルもpythonでトランスレーターが書けることがわかったので、トランスレーターを実装する準備を始めましょう。

## カスタムマテリアル(HDA)を作成する
カスタムマテリアルの作成は先ほどのドキュメントに手順が記載されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144656/6d15fd1b-da80-297b-55f3-4dd54b1f7334.png)

- FileメニューからNewAssetを選択
- VOPsを選択しNetworkTypeをUSD Surface Shaderに設定
- HDAを生成
- HDAが生成されたらNodeタブを開きRenderMaskに識別子を設定
-  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144656/942c548d-c66f-58dd-9e97-2d93ba1f059f.png)
- 今回は識別子に CUSTOMUSD と設定します（識別子は任意です）
- パラメータを設定。今回はベースカラーテクスチャ用に baseColor を設定しました
- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144656/8d29175a-c1ce-75c7-d34f-0dc5831af181.png)

Pythonトランスレーターとマテリアルの関連は Render Mask に設定されている識別子を使用します。これ重要です！

## トランスレーターを実装してみる
実装したトランスレーターの仕様は

- UsdPreviewSurface
- ファイルテクスチャをdiffuseColorへ設定

シンプルな仕様ですが、UsdPreviewSurfaceの生成とテクスチャの接続に対応していますので、diffuseColor以外のテクスチャも同様の実装で対応できます。コードをみていきましょう


```python
import re
from husd.shadertranslator import ShaderTranslator as TranslatorBase
from husd.shadertranslator import ShaderTranslatorHelper as HelperBase
from pxr import Sdf, UsdShade

class CustomShaderTranslatorHelper( HelperBase ):
    def __init__(self, translator_id, 
                 usd_stage, usd_material_path, usd_time_code):
        HelperBase. __init__( self, translator_id, 
                usd_stage, usd_material_path, usd_time_code )

    def usdRenderContextName(self, shader_node, shader_node_output_name):
        """ Returns the name of the render context used in the USD material 
            output name. In this case, a univarsal USD render context name. 
        """
        return UsdShade.Tokens.universalRenderContext

    def createUsdShader(self, shader_node, shader_node_output_name,
            usd_parent_schema):
        """ Creates and configures a single USD shader primitive 
            based on the given Houdini shader node.
            Returns a USD shader schema for the created USD primitive.
        """
        # create the USD shader
        usd_shader_path = self.usdShaderPrimitivePath(shader_node, 
                usd_parent_schema)

        # create st reader
        stReader = UsdShade.Shader.Define(self.usdStage(), usd_shader_path.AppendPath('streader'))
        stReader.CreateIdAttr('UsdPrimvarReader_float2')
        stReader.CreateInput('varname', Sdf.ValueTypeNames.Token).Set("st")

        # create texture
        filename = shader_node.parm('basecolor').eval()
        diffuseTextureSampler = UsdShade.Shader.Define(self.usdStage(), usd_shader_path.AppendPath('texture'))
        diffuseTextureSampler.CreateIdAttr('UsdUVTexture')
        diffuseTextureSampler.CreateInput('file', Sdf.ValueTypeNames.Asset).Set(filename)
        diffuseTextureSampler.CreateInput('st', Sdf.ValueTypeNames.Float2).ConnectToSource(stReader.ConnectableAPI(), 'result')
        diffuseTextureSampler.CreateOutput('rgb', Sdf.ValueTypeNames.Float3)

        # create preview shader
        usd_shader = self.defineUsdShader( shader_node, usd_shader_path )
        usd_shader.SetShaderId( 'UsdPreviewSurface' )
        # connect diffuse textire
        usd_shader.CreateInput('diffuseColor', Sdf.ValueTypeNames.Color3f).ConnectToSource(diffuseTextureSampler.ConnectableAPI(), 'rgb')

        return usd_shader

################################################################################
class CustomShaderTranslator( TranslatorBase ):
    def __init__(self):
        TranslatorBase.__init__( self )
        self.myPattern = re.compile( r'\bCUSTOMUSD\b' )

    def matchesRenderMask(self, render_mask):
        return bool( self.myPattern.search( render_mask ))

    def renderContextName(self, shader_node, shader_node_output_name):
        # Standard and USD preview shaders don't have render context name,
        # since they are applicable to all renderers.
        return UsdShade.Tokens.universalRenderContext

    def shaderTranslatorHelper(self, translator_id,
                usd_stage, usd_material_path, usd_time_code):
        return CustomShaderTranslatorHelper(translator_id,
                usd_stage, usd_material_path, usd_time_code)


standard_translator = CustomShaderTranslator()
def usdShaderTranslator():
    return standard_translator

```

ポイントとなるのは トランスレーター機能を提供する ShaderTranslator 、ShaderTranslatorHelper クラス と トランスレーターを登録する usdShaderTranslator です。

#### ShaderTranslator 
CustomShaderTranslator はトランスレーター本体で ShaderTranslator を継承しています。 

```python
class CustomShaderTranslator( TranslatorBase ):
    def __init__(self):
        TranslatorBase.__init__( self )
        self.myPattern = re.compile( r'\bCUSTOMUSD\b' )
```
マテリアルノードとの関連は Render Mask で行います。

```python

    def matchesRenderMask(self, render_mask):
        return bool( self.myPattern.search( render_mask ))
```
UsdPreviewSurface では UsdShade.Tokens.universalRenderContext を返せばいいらしい

```python
    def renderContextName(self, shader_node, shader_node_output_name):
        # Standard and USD preview shaders don't have render context name,
        # since they are applicable to all renderers.
        return UsdShade.Tokens.universalRenderContext
```

トランスレーターは ShaderTranslatorHelper へ実装していきます

```python
    def shaderTranslatorHelper(self, translator_id,
                usd_stage, usd_material_path, usd_time_code):
        return CustomShaderTranslatorHelper(translator_id,
                usd_stage, usd_material_path, usd_time_code)
```

#### ShaderTranslatorHelper 
CustomShaderTranslatorHelperを実装していきます。

```python
class CustomShaderTranslatorHelper( HelperBase ):
    def __init__(self, translator_id, 
                 usd_stage, usd_material_path, usd_time_code):
        HelperBase. __init__( self, translator_id, 
                usd_stage, usd_material_path, usd_time_code )

    def usdRenderContextName(self, shader_node, shader_node_output_name):
        """ Returns the name of the render context used in the USD material 
            output name. In this case, a univarsal USD render context name. 
        """
        return UsdShade.Tokens.universalRenderContext
```
トランスレーターをcreateUsdShaderへ実装します。

```python
    def createUsdShader(self, shader_node, shader_node_output_name,
            usd_parent_schema):
        """ Creates and configures a single USD shader primitive 
            based on the given Houdini shader node.
            Returns a USD shader schema for the created USD primitive.
        """
```
ShaderTranslatorHelper.usdShaderPrimitivePath関数でShader Primを生成します。

```python
        # create the USD shader
        usd_shader_path = self.usdShaderPrimitivePath(shader_node, 
                usd_parent_schema)
```
テクスチャ向けのPrimを作成していきます。

```python
        # create st reader
        stReader = UsdShade.Shader.Define(self.usdStage(), usd_shader_path.AppendPath('streader'))
        stReader.CreateIdAttr('UsdPrimvarReader_float2')
        stReader.CreateInput('varname', Sdf.ValueTypeNames.Token).Set("st")

        # create texture
        filename = shader_node.parm('basecolor').eval()
        diffuseTextureSampler = UsdShade.Shader.Define(self.usdStage(), usd_shader_path.AppendPath('texture'))
        diffuseTextureSampler.CreateIdAttr('UsdUVTexture')
        diffuseTextureSampler.CreateInput('file', Sdf.ValueTypeNames.Asset).Set(filename)
        diffuseTextureSampler.CreateInput('st', Sdf.ValueTypeNames.Float2).ConnectToSource(stReader.ConnectableAPI(), 'result')
        diffuseTextureSampler.CreateOutput('rgb', Sdf.ValueTypeNames.Float3)
```
UsdPreviewSurfaceを生成し、テクスチャをdiffuseColorへ接続します。

```python

        # create preview shader
        usd_shader = self.defineUsdShader( shader_node, usd_shader_path )
        usd_shader.SetShaderId( 'UsdPreviewSurface' )
        # connect diffuse textire
        usd_shader.CreateInput('diffuseColor', Sdf.ValueTypeNames.Color3f).ConnectToSource(diffuseTextureSampler.ConnectableAPI(), 'rgb')

        return usd_shader
```

変換に成功するとこのように表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144656/aaa0c903-2aea-36a5-9b81-892deba0b5d4.png)


```
#usda 1.0
(
    doc = """Generated from Composed Stage of root layer 
"""
    framesPerSecond = 24
    metersPerUnit = 1
    timeCodesPerSecond = 24
)

def Xform "grid1" (
    customData = {
        int[] HoudiniPrimEditorNodes = [21]
    }
    kind = "component"
)
{
    matrix4d xformOp:transform:xform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )
    uniform token[] xformOpOrder = ["xformOp:transform:xform"]

    def Mesh "mesh_0" (
        apiSchemas = ["MaterialBindingAPI"]
    )
    {
        float3[] extent = [(-5, 0, -5), (5, 0, 5)]
        int[] faceVertexCounts = [4] (
            customData = {
                int64 HoudiniDataId = 5583945335354111658
            }
        )
        int[] faceVertexIndices = [0, 1, 3, 2] (
            customData = {
                int64 HoudiniDataId = 5583945335354111658
            }
        )
        rel material:binding = </materials/CustomUSDShader1>
        uniform token orientation = "leftHanded"
        point3f[] points = [(-5, 0, -5), (5, 0, -5), (-5, 0, 5), (5, 0, 5)] (
            customData = {
                int64 HoudiniDataId = 31
            }
            interpolation = "vertex"
        )
        texCoord2f[] primvars:st = [(-4.9999995, 5), (4.9999995, 5), (4.9999995, -5), (-4.9999995, -5)] (
            customData = {
                int64 HoudiniDataId = 39
            }
            interpolation = "faceVarying"
        )
        int[] primvars:st:indices = None
        uniform token subdivisionScheme = "none"
    }
}

def HoudiniLayerInfo "HoudiniLayerInfo" (
    customData = {
        int HoudiniCreatorNode = 19
        int[] HoudiniEditorNodes = [19, 20, 16, 21, 24]
        string HoudiniSaveControl = "Placeholder"
        bool HoudiniTreatAsSopLayer = 0
    }
)
{
}

def Scope "materials"
{
    def Material "CustomUSDShader1" (
        customData = {
            int[] HoudiniPrimEditorNodes = [25]
        }
    )
    {
        token outputs:surface.connect = </materials/CustomUSDShader1/CustomUSDShader1.outputs:surface>

        def Shader "CustomUSDShader1"
        {
            uniform token info:id = "UsdPreviewSurface"
            color3f inputs:diffuseColor.connect = </materials/CustomUSDShader1/CustomUSDShader1/texture.outputs:rgb>
            token outputs:surface

            def Shader "streader"
            {
                uniform token info:id = "UsdPrimvarReader_float2"
                token inputs:varname = "st"
                float2 outputs:result
            }

            def Shader "texture"
            {
                uniform token info:id = "UsdUVTexture"
                asset inputs:file = @C:/Users/hideki/Desktop/USDLogoHeader.png@
                float2 inputs:st.connect = </materials/CustomUSDShader1/CustomUSDShader1/streader.outputs:result>
                float3 outputs:rgb
            }
        }
    }
}

```

## おわり
OBJ編、マテリアル編で、「あ～この情報がLOPへ持っていけたらいいのになぁ 」というケースで助けになればと思い記事にしました。改めてSolaris(LOP)良くできているなと思いました。
もしSolarisの習得に壁を感じている方（特にTA）は、USDをPython APIやusda(USDテキスト形式)を見てからSolarisへ戻ると理解しやすいかもしれないです。
なんだか駆け足で申し訳ないです・・・・

githubへHDAとPythonコードをアップしました。
https://github.com/K240/ac2021usd