前回に続き,今回はテクスチャマッピングを行います。用いる画像は USD のロゴマークとします。
テクスチャマッピング
テクスチャリーダーとプリミティブ変数リーダー
前回は PBR シェーダーの diffuseColor や opacity に特定の色(マゼンダ)や特性を割り当てましたが,今回はここにテクスチャを割り当てます。そのために,テクスチャリーダー(UsdUVTexture)とプリミティブ変数リーダー(UsdPrimvarReader_float2)の 2 つのシェーダーノードを用意します(これらについては公式の仕様書に詳しい説明があります)。
まずは,テクスチャリーダーから。file アトリビュートにテクスチャ(画像ファイル)のパスを指定します。マッピングに使用する st 座標 はマテリアルに張るメッシュを参照して得るようにするので,ここでは初期化しません。出力は rgb と a で,それぞれこのあとのプログラムでシェーダーの diffuseColor と opacity に接続されます。
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 型のリーダーだけを準備しておけば充分です。
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
varname のデータ型は Token ではなく String にします。コロンなどの特殊文字の対応のためだと思われます。たしかにここのプリミティブ変数リーダーの定義でも string になっています。誤って Token にしてしまった場合は usdcheker に掛けると間違いを指摘してくれます。
メッシュノードの修正
前々回作成したメッシュノードを修正して,UV 座標データを与えます。一例として,下図のように左右の面で異なる向きの座標を与えてみます(右は90度回転している)。
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 情報を取得するための接続ターミナルを用意します。
def create_material(stage, path):
material = UsdShade.Material.Define(stage, path)
# Inputs
material.CreateInput('frame:stPrimvarName', Sdf.ValueTypeNames.Token).Set('st')
# Outputs は前回と同じ
...
return material
接続
シェーダー,マテリアル,メッシュを接続します。本件ではメインプログラムでこれを行うことにします(ステージ作成からシェーダー作成までは前回までの記事と同じです)。
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 もパスしています。
#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
}
}
}
$ usdchecker Material.usda
Success!
マテリアルを表示してみます。左右ふたつの面にそれぞれロゴがマッピングされました(右側は90度回転している)。
まとめ
テクスチャの情報をPBRシェーダのパラメータに繋ぎながら,マテリアルへのマッピングを実現しました。
参考資料
ft-lab (Yutaka Yoshisaka) さん,あんどうめぐみさん,OpenUSD公式 による以下のサイトが理解の助けになりました。