MaterialX


この記事は、レイトレ合宿5のアドベントカレンダーの記事です。

今回のアドベントカレンダー、レベル高すぎるよ!
今までレベル高すぎたので、この辺で、全然役に立たない記事をぶっこんでも大丈夫ですよね!



皆さんこんにちは、八田です。レイトレ順調でしょうか?
自分はまだ現時点で1行もコードを書いていません!!!

正確には、3月くらいにMMDBridgeRT というNVIDIA Optix 製のレンダラーを
MikuMikuDance用に作りましたが、これは CUDA 必須であり、レイトレ合宿では残念ながら使えません。
これについて言いたいことは1つだけで、TwitterでMMDBridgeRT で検索してみてください。
なぜか、イケメンの画像等が大量に出てきますよね??
日本ではパストレでイケメンの男モデルをレンダーする需要があるッ…!(女性にも人気!)



さて、今日は23日、ニーサの日です。
ニーサとは何か? NISA と書きます。
これを申し込むとですね、株などの売買で税金が掛からないのです。
昨年、高騰する合宿参加費を賄うために株に手を出しましたが、開始して丁度1年が経過しました。
ここでNISAを使って取引した、1年の僕の成績を発表します。

 約-13万円です(内2万円はFX)

レイトレ合宿でもボコボコでしたが、株でもFXでもボコボコに負けています。
どちらも勉強にはなりますが勝てる気がしません!

実は、今日はここに僕の詳細な取引履歴を張り付けて、アドベントカレンダーをやり過ごそうかと
考えていたんですが、これを見てください。

記事を書いている現在、メンテナンス中です。とことんついていません。

しかし、お金については1つ秘策を見出しました。今年制度が拡大した確定拠出年金に加入するのです。
1か月1万円定年まで払えば、そのお金は定年くらいまで帰ってきませんが、100万円くらい税金が浮きます。
2万円だと200万です。今回のレイトレ合宿も参加費は2万円くらいだった気がしますが、余裕ですね!早速来週申し込もうと思います。
皆さんも確定拠出年金なりふるさと納税なりをじゃんじゃんして節税したお金でレイトレ合宿に参加しましょう。

さて、レイトレ合宿のほうですが、何も策がありません!!
今から策を編み出すべく開始します(今この瞬間から)。





MaterialX

今回は MaterialX についてです。
前回の SIGGRAPH で、仕様書が pdf 1枚 公開されただけで、首を長くして待っていましたが、

昨年調べた情報(古いので注意)によると、マテリアル情報をXMLに書いて定義できる、外付けのファイルフォーマットで、いろいろなシーングラフや3Dオブジェクトに対してマテリアルを定義でき、シェーダーまでぶっ込めるもののようです。

考えてみると、世の中には共通のマテリアルフォーマットは皆無でした。
Substance Painter というツールがありますが、SDKは公開されていなくてSDK利用は企業からしても爆高いという噂ですし、
NVIDIA MDLとかプロプライエタリなものは最近出てきましたが、オープンソースではありません。
シーン交換のためのファイルフォーマットである、Alembicも、Materialは去年ようやく基本的な部分がついた程度です。
Pixar USDはAlembicに対応しており、さらに独自のメタデータやら何やらを自由に入れられるようですが、
シーンデータ交換のためには、ある程度の縛りが必要と思います。
このMaterialXは、前回調べたところによると、ある程度フォーマットが縛られていてAlembicと併用可能で、大手が作ってることもあり、期待ができます。




早速ビルドしていきます。
git cloneして、いつも通り CMakeします。VS2015でやっています。
特に何事もなく、configure が終わるんですが、MATERIALX_BUILD_PYTHONというチェックがあるようなので
当然チェックを入れて、generateします。



ソリューションができたので、早速 Release でビルドします。


どうやらPythonを使っているプロジェクト以外はビルド成功しているようです。
これはビルド楽勝っぽいですね!

残りのビルド(PyMaterialX)を成功させるために、Python2.7系の64bit版をインストールします。
インストール場所は、プロジェクトファイル見る限り、デフォルトの C:\Python27で良さそうです。

Pythonのバインディングは、pybind11を使ってるようです。
このライブラリはboost::pythonよりもお手軽に使えるヘッダーオンリーのライブラリでとても良いです。最近shared_ptrにも対応しました。MaterialXではpybind11のshared_ptrのバインドも使っているようですね。
ついでに書いておくと、他にはユニットテストに Catch 、XML入出力に PugiXML というのを使っているようです。


Python27をインストールすることで、無事ビルドが成功しました。
出来上がったブツ(のうち使うと思われるもの)は以下の通り。
  • MaterialXCore.lib
  • MaterialXFormat.lib
  • PyMaterialX.pyd
  • MaterialXTest.exe
Testのexeができていますが、まぁこれはただの単体テスト用でしょう。ライブラリの使い方を見るときには参考になります。
ライブラリなので、Pythonからpydを使うか、C++からMaterialX~.libを使うことになると思います。



早速使っていきます。
ビルドが面倒なのでpython版を使ってみます。

import PyMaterialX as mx
print dir(mx)

とりあえずこのようなtest.pyファイルをPyMaterialX.pyd と同じディレクトリに作り、
C:\Python27\python.exe test.py

として実行します。もしくはインタラクティブコンソールでお試ししても良いです。
すると、以下のような出力が出ます。



無事ビルド成功しているようです。
ではじゃんじゃん行きます。MaterialXTestのDocument.cppを見ながら

# coding: utf-8
import PyMaterialX as mx

# ドキュメント新規作成
doc = mx.createDocument()

# ノードグラフの作成
nodeGraph = doc.addNodeGraph()

# ノードグラフに constant ノードを追加
# C++では addNode() 
constant = nodeGraph._addNode("constant")

# constant ノードにパラメータを設定
# C++では setParameterValue()
constant._setParameterValuecolor3("value", mx.Color3(0.5, 0.5, 0.5))

# 出力ノードの追加
output = nodeGraph.addOutput()
output.setConnectedNode(constant)

# XMLファイル文字列を表示
print mx.writeToXmlString(doc)

これでXMLが出力されます。このXMLがMaterialX( .mtlx )です。
ノードエディタでノード作ってるみたいな感じのコードで分かりやすいですね。


微妙に関数がC++とPythonで違うのが嫌ですが、"_"付きなので、そのうち変更されるかもしれません。
公式のPythonサンプルコードでは、"_"無しで書いていますが、もちろん現状それでは動きません。

とりあえず以下のコードを付け足してファイルにも保存しておきましょう。

# XMLファイル出力
mx.writeToXmlFile(doc, "test.mtlx")


できました!
一応読み込みもやっておきます。

# coding: utf-8
import PyMaterialX as mx

# ドキュメント新規作成
doc = mx.createDocument()

# mtlxファイルから読み込み
mx.readFromXmlFileBase(doc, "test.mtlx")

# 有効なドキュメントか?
print doc.validate()

問題なく True になります。

ついでに前回調べたとき(=ソース公開前)にBlenderのプラグインから出力して作った、オレオレmtlxファイルを読み込んでみたところ、validate()でTrueが返ってきました!普通にtraverseもできる。ちょっとうれしい。ただ、opgraphなどのエレメントは現行仕様では消滅しており、nodegraphなどという名前に変わっている。コードを漁ると、ちゃんと過去のバージョンもサポートしているので使えるようではあるが…

さて、妙にノードっぽいコードでしたが、MaterialXTestのサンプルを見ていると、Node.cppにこんなコメントがあります

TEST_CASE("Topological sort", "[nodegraph]")
{
    // Create a document.
    mx::DocumentPtr doc = mx::createDocument();

    // Create a node graph with the following structure:
    //
    //   [constant1] [constant2]      [image2]
    //           \   /          \    /
    // [image1] [add1]          [add2]
    //        \  /   \______      |   
    //    [multiply]        \__ [add3]         [noise3d]
    //             \____________  |  ____________/
    //                          [mix]
    //                            |
    //                         [output]
    //

もうこれどっからどうみてもノードエディタで使ってくれ(ノードエディタ作ってくれ)と言っているようにしか見えません!Topological Sortってノードエディタ作るときには大抵やりますよね。MaterialX = ノードベースのマテリアルフォーマット、と考えて問題ないと思います。

他、ちょっと目についたところとしては、Observerの仕組みが組み込まれているようです。Observe可能なDocumentを作成することができ、それに対してObserverを複数登録できます。これにより、アプリケーション内でマテリアルを編集した時等の通知を受け取ることができます。



次に、 .obj ファイルのMTL の代わりのmtlxファイルを作ってみることにします。


手始めにめちゃくちゃシンプルなやつで。


objファイルとしてはこんな感じ
(cabbage.obj)
# Blender v2.78 (sub 0) OBJ File: ''
# www.blender.org
mtllib cabbage.mtl
o cabbage
v 0.518277 1.045460 -1.303864
v 0.745456 0.803751 -1.099580
v -0.926315 2.123996 0.604743
v 0.008301 1.787386 1.152170
v 0.669115 1.202087 -0.994056
・・・

mtlファイルとしてはこんな感じ
(cabbage.mtl)

# Blender MTL File: 'None'
# Material Count: 2

newmtl cabbage
Ns 92.156863
Ka 1.000000 1.000000 1.000000
Kd 0.398431 0.398431 0.398431
Ks 0.000000 0.000000 0.000000
Ke 0.000000 0.000000 0.000000
Ni 1.000000
d 1.000000
illum 2
map_Kd cabbage_diff.jpg

これのmtlxファイルを作ります。

MaterialXの構造は以下のようになっています。


やることは恐らく以下の通りです。
  • 1. ドキュメントを作る
  • 2. Geometry を定義する。(必要ならGeomAttrもつける。図中ののtxtidというのは連番管理したりするとき用)
  • 3. NodeGraphを作る。(nodegraphは図の通り複数作れる)
  • 4. NodeGraphにノードを追加する(Image, output等)
  • 5. Materialを作る
  • 6. シェーダーを作ってMaterialに割り当てる。
  • 7. NodeGraph内の、ノードの接続
  • 8. NodeGraphの出力からMaterial内のシェーダー入力への、ノードの接続
  • 9. Look を作成して、MaterialとGeometryを紐づける
これを行うとこういうコードになります。

# coding: utf-8
import PyMaterialX as mx

# 1. ドキュメント新規作成
doc = mx.createDocument()

# 2. ジオメトリ定義の追加
geominfo1 = doc.addGeomInfo("geominfo1", "cabbage")
geominfo1._setGeomAttrValuestring("description", u"これはキャベツ", "")

# 3. ドキュメントにノードグラフを作成
nodeGraph = doc.addNodeGraph()

# 4. ノードグラフに出力ノードの追加
output = nodeGraph.addOutput()

# 4. ノードグラフにimageNodeを追加
imageNode = nodeGraph._addNode("image", "image_node")
imageNode._setParameterValuestring("file", "cabbage_diff.jpg") # パスを設定

# 5. Mateiralの作成
material = doc.addMaterial()

# 6. Shader の作成
shader = doc.addNodeDef("shader1", "surfaceshader", "simpleSrf")
Ns = shader.addInput("Ns", "float")
Ka = shader.addInput("Ka", "color3")
Kd = shader.addInput("Kd", "color3")
Ks = shader.addInput("Ks", "color3")
Ke = shader.addInput("Ke", "color3")
Ni = shader.addInput("Ni", "float")
d = shader.addInput("d", "float")
illum = shader.addInput("illum", "float")

Ns._setValuefloat(92.156863)
Ka._setValuecolor3(mx.Color3(1.000000, 1.000000, 1.000000))
Kd._setValuecolor3(mx.Color3(0.398431, 0.398431, 0.398431))
Ks._setValuecolor3(mx.Color3(0.000000, 0.000000, 0.000000))
Ke._setValuecolor3(mx.Color3(0.000000, 0.000000, 0.000000))
Ni._setValuefloat(1.000000)
d._setValuefloat(1.000000)
illum._setValuefloat(2.000000)

# 6. MaterialにShaderを登録
shaderRef = material._addShaderRef("shaderRef1", "simpleSrf")

# 7. imageNode → ノードグラフのoutput と接続
output.setConnectedNode(imageNode)

# 8. output → Kd と接続
bindInput = shaderRef.addBindInput("Kd")
bindInput.setConnectedOutput(output)

# 9. Lookの作成
#    Lookにmaterialassignを入れることにより material → Look ← Geometory という接続を作る.
look = doc.addLook()
materialAssign = look.addMaterialAssign("", material.getName())
materialAssign.setGeom("cabbage")

# XMLファイル文字列を表示
print mx.writeToXmlString(doc)
# XMLファイル出力
mx.writeToXmlFile(doc, "test.mtlx")

出来上がったファイルはこんな感じ

<?xml version="1.0"?>
<materialx version="1.35">
  <geominfo name="geominfo1" geom="cabbage">
    <geomattr name="description" type="string" value="これはキャベツ" />
  </geominfo>
  <nodegraph name="nodegraph1">
    <output name="output1" type="color3" nodename="image_node" />
    <image name="image_node" type="color3">
      <parameter name="file" type="string" value="cabbage_diff.jpg" />
    </image>
  </nodegraph>
  <material name="material1">
    <shaderref name="shaderRef1" node="simpleSrf">
      <bindinput name="Kd" type="color3" nodegraph="nodegraph1" output="output1" />
    </shaderref>
  </material>
  <nodedef name="shader1" type="surfaceshader" node="simpleSrf">
    <input name="Ns" type="float" value="92.1569" />
    <input name="Ka" type="color3" value="1, 1, 1" />
    <input name="Kd" type="color3" value="0.398431, 0.398431, 0.398431" />
    <input name="Ks" type="color3" value="0, 0, 0" />
    <input name="Ke" type="color3" value="0, 0, 0" />
    <input name="Ni" type="float" value="1" />
    <input name="d" type="float" value="1" />
    <input name="illum" type="float" value="2" />
  </nodedef>
  <look name="look1">
    <materialassign name="materialassign1" material="material1" geom="cabbage" />
  </look>
</materialx>

validate()でもTrueがちゃんと返ってきます。

うーん、なんかめっちゃ色々あって、お手軽・・・では無いですね。

MTLではmap_Kdのテクスチャは、Kdとすり替えて使うのか、Kdとミックスして使うのか、Kdと乗算するのか、そのあたりは仕様を読まないと分からなかったですが、MTLXでは、ノードの接続によりimageをKdに接続しているのは明らかなので、Kdのvalueとしてimageを使うというのが明確であるような気がします。

これを表示するためのコードを書くのは結構しんどいので、このあたりで調査終了ということにしておきます。
今後は、Blenderからmtlxを書き出す自作プラグインを、MTLXの最新仕様に沿って書き換えたりしたいと思います。




以上がMaterialX セカンドインプレッションになります。
今年の SIGGRAPH では、Pixar から OpenTimelineIO というものが公開されるようでそちらも楽しみですね。




Comments