2010-12-13

python on Nuke 3

前回は長々となってしまったので、分割しました。
引き続き、保存されたnukescript(.nk)のファイルパスは

C:/test/nukeScripts/teapot_composit_001.nk

Readノードにおけるファイルパスは

C:/test/cgRender/101209/teapot/teapot_%04d.exr

です。前回は、このReadノードが選択されている状態で、

---------------------------------------------------------
readpath = nuke.selectedNode().knob('file').value()
listread = readpath.split('/')

rootpath = nuke.root().name()
listroot = rootpath.split('/')

i = 0
while (listread[i] == listroot[i]):
        i = i + 1
--------------------------------------------------------- 

とするとこで、お互いのファイルパスのどこまでが一致しているかを判定しました。
前回も説明しましたが、

[file dirname [value root.name]]


という文字列を用いると用いると保存されたnukescript(.nk)のファイルパスから見たファイルパスで置き換えることができます。

[file dirname [value root.name]]/../cgRender/101209/teapot/teapot_%04d.exr

で、これがどういう利点があるかというと、Readノードにおけるファイルパスを相対パスに書き換えることができるということで、そうすることで、環境の違いやOSの違いにも対応しやすくなり、もちろんネットワークレンダリングする場合などにとても便利です。

「..」はひとつ階層があがるということです。[file dirname [value root.name]] は保存されたnukescript(.nk)のファイルパスを示しており、この場合

[file dirname [value root.name]] =  C:/test/nukeScripts/

ということになり、Readノードにおけるファイルパス「C:/test/cgRender/101209/teapot/teapot_%04d.exr」と比べると、「C:/test/」までしか一致しないので、その一致する階層に移動するために、[file dirname [value root.name]] =  C:/test/nukeScripts/ から一つあがる必要があります。

その一つあがるが

[file dirname [value root.name]]/../cgRender/101209/teapot/teapot_%04d.exr

での「..」となります。
毎度毎度くどい説明ですが、そんなです・・・
ちなみに、前回用いたリスト、「listroot」と「listread」をわかりやすくすると、



となります。ここでこれらタプル型のリストの文字列の数は、

len()

で示すことができて、たとえば、listrootに含まれる文字列の数は、

len(listroot)

で表すことができます。この場合、

print len(listroot)

をすると



となり、「4」という数字が帰ってきます。


で、わかるようにリストの構成文字列は4個です。最後の listroot[3] に相当する、「teapot_composit_001.nk」は階層ではなくファイルそのものなので、階層だけで考えると、3個の階層で、さらに、前回、頭から2個目までが一致しているという結果を得ているので、

(4 - 1) - 2 = 1

をして、[file dirname [value root.name]] =  C:/test/nukeScripts/ から上がる階層としては、1階層分となります。なので [file dirname [value root.name]] にその回数分(上がる階層分) 「..」を追加します。またここで、リスト「listroot」とリスト「listread」に含まれる文字列の一致回数と、「..」の追加回数を適当に、「sametimes」、「uptimes」として、そのコードとし、前回分のコードからの続きで・・・

---------------------------------------------------------

readpath = nuke.selectedNode().knob('file').value()
listread = readpath.split('/')

rootpath = nuke.root().name()
listroot = rootpath.split('/')

i = 0
while (listread[i] == listroot[i]):
        i = i + 1

sametimes = i


uptimes = len(listroot) - sametimes - 1
---------------------------------------------------------

と書き表すことができます。ループ文(この場合whileループ)などは段がえを抜けると、次のブロックになるので、whileループは「i = 2」で終わり、「sametimes」にそれが適応されてます。

で、ここで得たuptimes分「..」を [file dirname [value root.name]] に付け加えます。
[file dirname [value root.name]]を一旦「reppath」という文字列で置き換えて・・・

---------------------------------------------------------
readpath = nuke.selectedNode().knob('file').value()
listread = readpath.split('/')

rootpath = nuke.root().name()
listroot = rootpath.split('/')

i = 0
while (listread[i] == listroot[i]):
        i = i + 1

sametimes = i
uptimes = len(listroot) - sametimes - 1

reppath = '[file dirname [value root.name]]' 

for i in range(uptimes):
   
reppath = reppath + ('/..')

print reppath
 --------------------------------------------------------- 

としてやります。すると結果として・・・


 となり、

# Result: [file dirname [value root.name]]/..

を得ることができます。
ここではforループというものを用いています。

for i in range (数字):
    行いたい処理

としてやると、「数字」の回数分だけ行いたい処理を行います。たとえば数字に「5」を入れて、print i とかすると・・・


--------------------------------------------------------
for i in range(5):
    print i 
--------------------------------------------------------


となり、

# Result: 0
1
2
3
4

を得ます。気をつけたいのは、「数字」を「5」を入れると「0,1,2,3,4」を順番に「 i 」に代入していき、「行いたい処理」を実行するということです。今回の場合はあまり関係がないのですが・・・
この場合、

for i in range(uptimes):
    reppath = reppath + ('/..')


で「uptimes」は「1」なので、一回だけ

reppath = reppath + ('/..')

の処理がされます。たとえば、もし「2」なら、一回目で
reppath = reppath + ('/..')  =  '[file dirname [value root.name]]/..'

となっている、reppathに対して、

reppath = reppath + ('/..')

が行われますので、

reppath = reppath + ('/..')  =  '[file dirname [value root.name]]/../..'

となり、その分、階層を上がっていくことになります。ちょっとそれましたが・・・

続けていきましょう。さらに、ここまでで得た、

reppath = reppath + ('/..')  =  '[file dirname [value root.name]]/..'

に、Readノードにおけるファイルパス C:/test/cgRender/101209/teapot/teapot_%04d.exr をリスト化した「listread」から必要な分だけ、文字列を抽出して追加していきます。この追加回数を、適当に「addtimes」とします。で「addtimes」は、「listread」に含まれる文字列の数から、リスト「listroot」とリスト「listread」に含まれる文字列の一致回数「sametimes」を引いたものになり、さらに、「listread」から抽出したいリストの番号を「ordtimes」として、

---------------------------------------------------------
readpath = nuke.selectedNode().knob('file').value()
listread = readpath.split('/')

rootpath = nuke.root().name()
listroot = rootpath.split('/')

i = 0
while (listread[i] == listroot[i]):
     i = i + 1

sametimes = i
uptimes = len(listroot) - sametimes - 1

reppath = '[file dirname [value root.name]]' 

for i in range(uptimes):
    reppath = reppath + ('/..')

addtimes = len(listread) - sametimes

for i in range(addtimes):
   
ordtimes = sametimes + i
 
    reppath = reppath + '/' + listread[ordtimes]

print reppath
 ---------------------------------------------------------

とすると、



となり、

# Result: [file dirname [value root.name]]/../cgRender/101209/teapot/teapot_%04d.exr

を得ました。


ですので、

addtimes = len(listread) - sametimes

で導かれる数字「4」を用いて、

for i in range(addtimes):
    ordtimes = sametimes + i
    reppath = reppath + '/' + listread[ordtimes] 

で4回同じ処理をするのですが、「i」 には 「0、1、2、3」 が順番に代入されます。ここで、前に定義した「reppath」の後ろに listread[2]、listread[3]、listread[4]、listread[5] を順に追加していきたいので、一致回数「2」の「sametimes」を用いて、

ordtimes = sametimes + i

として、i = 0 の時

reppath = reppath + '/' + listread[ordtimes]  =  '[file dirname [value root.name]]/../' + listread[2] 

となり、その時、

reppath = '[file dirname [value root.name]]/../cgRender'

となります。あとはループですので、この文字列に追加すべく文字列が追加されていきます。

で結果、

# Result: [file dirname [value root.name]]/../cgRender/101209/teapot/teapot_%04d.exr

を得ることができるので、あとは、これを用いて、選択しているReadノードのファイルパスに置き換えてやればいいので、

---------------------------------------------------------
readpath = nuke.selectedNode().knob('file').value()
listread = readpath.split('/')

rootpath = nuke.root().name()
listroot = rootpath.split('/')

i = 0
while (listread[i] == listroot[i]):
     i = i + 1

sametimes = i
uptimes = len(listroot) - sametimes - 1

reppath = '[file dirname [value root.name]]' 

for i in range(uptimes):
    reppath = reppath + ('/..')


addtimes = len(listread) - sametimes

for i in range(addtimes):
    ordtimes = sametimes + i
    reppath = reppath + '/' + listread[ordtimes]

nuke.selectedNode().knob('file').setValue(reppath)

---------------------------------------------------------

です。


ここでやっているように、

nuke.selectedNode().knob('file').setValue(reppath)

とすると、実際にReadノードのファイルパスが 「.setValue(reppath)」によって、「reppath」である

[file dirname [value root.name]]/../cgRender/101209/teapot/teapot_%04d.exr

によって置き換わります。


と、こんな感じで、自前でファイルパスを変更するツールができました。
更に関数化することで、シーン内のReadノードに対して一気に処理できたり、色々とより便利なものへとしていくことも可能です。


・・・ しかし、くどい説明・・・

逆に理解しづらいかも・・・ わかり難いところあれば、コメントいただけると幸いです。

補足:
前回も何事も無く触れてないのですが、文字列を扱う時は「' '(シングルクオート)」で囲わなければなりません(「" "(ダブルクオート)」でもOKです)。
たとえば適当なリスト「ls」をでっち上げたとして、そのリストの中に「apple」、「gorilla」、「trumpet」を格納したい場合は

ls = ['apple' , 'gorilla' , 'trumpet']

です。数字なら、「' '(シングルクオート)」を用いずに、

ls = ['apple' , 'gorilla' , 'trumpet' , 0 , 1 , 2]

となります。

2010-12-09

python on Nuke 2

ってことで続きです。

前回「タプル」という言葉を出しましたが、「タプル」アマチュアのボクが考えるに、「タプル」はまさにリストです。

話はいきなり飛びますが、Nukeはファイルパスをそのnukescript(.nk)のある場所から見たパスに置き換えることができます。いわゆる、相対パスです。相対パスに置き換えれれば、環境が変わっても、ディレクトリ構造が一緒であれば、ファイルパスに関しては問題なく同じように扱えます。
また、たとえば、ローカルで作業して、ファイルサーバー上に同じディレクトリ構造で同じようにファイルを持っていれば、ネットワークレンダリングなどで、ファイルパスを気にせず分散処理ができます。

実際どういうことかというと・・・
今ココに、適当な連番ファイルを読み込んだReadノードを持つ「teapot_composit_001.nk」というnukescript(Nukeのプロジェクトファイル)があるとします。





図は模式図ですが、こんな階層構造になっていて、 nukescriptのファイルパスは

C:/test/nukeScripts/teapot_composit_001.nk


 であり、Readノードに記されたファイルパスは

C:/test/cgRender/101209/teapot/teapot_%04d.exr


です。
で、この場合、 このReadノードのファイルパスを

[file dirname [value root.name]]/../cgRender/101209/teapot/teapot_%04d.exr


 と相対パス表示に置き換えることができ、これはディレクトリ構造的にnukescriptのファイルパスから見て、Readノードの相対パスが保たれていれば、どこへ持っていってもファイルが見つからないということはありません。win、mac、linux どれであっても問題なくファイルパスが通ります。

[file dirname [value root.name]]

というのは、そのnukescriptが存在するディレクトリを指しています。この場合、「nukeScripts」ディレクトリの一階層上にある「cgRender」ディレクトリからたどらないといけないので、「一階層上」を表すため、

[file dirname [value root.name]]/../cgRender/101209/teapot/teapot_%04d.exr

「..」が挿入されています。


で、この方法を用いてpythonで、この部分を自動処理させます。

まずは、Readノードのファイルパスを取得します。そのスクリプトの運用方法にもよりますが、ひとまず、Readノードを選択した状態で、その連番のファイルパスがpython上ではどうなるかを見ていきます。

まず、選択したノードは前回にも説明しましたが、単一選択の場合は

nuke.selectedNode()

で表されます。
さらに、そのファイルパスは・・・

nuke.selectedNode().knob('file').value()


で表されます。 実際にはこのファイルパスは

C:/test/cgRender/101209/teapot/teapot_%04d.exr

です。さらに、これと、 nukescriptのファイルパスを見比べて、クダンの文字列と入れ替えるのですが、そのnukescriptのファイルパスは、

nuke.root().name()



で表されます。これはNukeの中ではこういうものみたいです。
また、これらのファイルパス(文字列)を見比べたいのですが、階層の数などを考慮するため、「/(スラッシュ)」を取り除いてリスト化しようと思います。ここでまさにpython的効力が発揮されるわけですが、一旦ファイルパスを扱いやすいように、

readpath = nuke.selectedNode().knob('file').value()

として ファイルパスを一旦「readpath」で置き換えます。さらに同じく、nukescriptのファイルパスも


rootpath = nuke.root().name()


として、同様に「rootpath」で置き換えます。でこの文字列が格納された、「readpath」、「rootpath」をpythonの機能(関数)を用いて、、「/(スラッシュ)」を取り除いてリスト化するという処理を施します。用いる関数は「split」というもので、

readpath.split('/')

とすると、スラッシュを取り除いてできる文字列群のリストが出来上がります。
実際に、


print readpath.split('/')


とすると、


# Result: ['C:', 'test', 'cgRender', '101209', 'teapot', 'teapot_%04d.exr']



と返ってきます。これがタプル型に格納された文字列のリストになります。
このままでは扱いづらいので、

listread = readpath.split('/')

として、「listread」というものでこのリストを表します。またリストは頭から数えて何番目かの文字列を取り出す、ということができます。たとえば、

print listread[3]

とすると、

# Result: 101209


が返ってきます。1番目は[0]なので、[3]とすることで、4番目の文字列が返ってきました。

print listread[5]

にすれば、6番目の文字列である

# Result: teapot_%04d.exr

と返ってくるはずです。

じゃあ、ここで、Readノードのファイルパス と nukescriptのファイルパス を見比べて、リストの何個目まで文字列が一致するかを調べてみます。whileループというものを使います。

whileループは条件を指定し、その条件が満たされている限りは、指定した処理を行うというものです。実際には

while (条件文):
        行いたい処理

となります。今回の場合リストに格納されている文字群がどこまで一緒かを調べたいので、「i」という適当な変数を用いて、Readノードが選択されている状態で、

---------------------------------------------------------
readpath = nuke.selectedNode().knob('file').value()
listread = readpath.split('/')

rootpath = nuke.root().name()
listroot = rootpath.split('/')

i = 0
while (listread[i] == listroot[i]):
        i = i + 1
---------------------------------------------------------

とします。この場合条件文が

listread[i] == listroot[i]

で、行いたい処理が

i = i + 1

です。
あらかじめ、 i = 0 と宣言しているので、
まず最初に、

listread[0] == listroot[0]

が成り立っているか否かをしらべます。成り立っている場合は、

i = i + 1

の処理が行われ、今一度、条件文の判定が行われます。すると、最初は i = 0 でしたが、 i = i + 1 によって、 次の条件文では i = 1 となり

listread[1] == listroot[1]

が成り立っているか否かを調べることになります。このケースの場合、まだ、この時点では成り立っているので、i = i + 1 処理が行われ、この時点で i = 2 となります。whileはその条件文が成立しなくなるまで処理を続けるので、この時点での条件文、


listread[2] == listroot[2]

が成立しなくなります('cgRender' と 'nukeScripts')。なので、 i = i + 1 処理は行われず、 i = 2 という結果を得ます。
つまり、頭から二つ目までが一致しているということになります。



長くなっているので、今回はここまでにします。

補足:pythonでは(?)、ループ処理などで「条件文」と「行いたい処理」を「:(コロン)」で区切って、改行し、さらに「段違い」にします。処理が複数個あるときはその「段違い」で頭をそろえる必要があります。

たとえば、

--------------------------------------------------------- 
while (listread[i] == listroot[i]):
        i = i + 1
        i = i - 1
---------------------------------------------------------
というような感じです。

2010-11-23

python on Nuke

Nukeでのpythonについて。
まずは、簡単なところから・・・

適当にUIを変更してpythonを編集しやすいようにしておく。 フレームの適当なところで右クリックすると、カスタマイズや追加できるwindowが出るので、その中から「Script Editor」を選んで、pythonをいじれるように調整する。




Script Editorは上がOutput(結果等が表示される)で、下がinput(実際にコードを書いて実行するところ)だ。


 また、PreferenceのScript Editorのタブで「echo python commands to output window」にチェックを入れておけば、全てではないがオペレーションがOutputに反映されるので、ある程度参考にはできる。ただmayaとかの同様の機能ほど優秀でないので、その都度リファレンスなどを見ながら欲しいコマンドを探す必要はあるw

準備が整ったところで、とりあえずpythonで適当な操作を・・・

とりあえず、たとえば新規でBlurノードを作ってみる。

nuke.hogehogehoge.....

というような感じで「nuke.」から始まるコマンドでNuke内のコマンドを呼び出せる。
新たにノードを呼び出し作成する場合は「createNode('hoge')」を用いる。Blurノードを呼び出しければ、

 nuke.createNode('Blur')

を実行すれば


こんな感じで output window に結果が表示され、NodeGraphにBlur1という新たなBlurノードが確認できる。

次に、たとえば、ノードのアトリビュートを操作してみる。まずは、 このBlurノードの[size]の値を取得してみる。「nuke.」の後ろに、ノード名を指定してさらに、そのアトリビュート、この場合[size]の値を・・・という感じで、上から順に重ねていく。実際には・・・

nuke.toNode('Blur1').knob('size').value()

となる。通常ではこの頭に「print」などを付けて値をアウトプットさせるということが多いと思う。付けなくてもoutput windowには表示されるが・・・


値を変更したい場合は「setValue()」をもちいて、

 nuke.toNode('Blur1').knob('size').setValue()

とする。実際には、 「setValue()」の括弧の中に数値などを挿入する。

  nuke.toNode('Blur1').knob('size').setValue(3)
 
こんな感じ。すると・・・


こんな感じで値が変更される。

さらに例えば選択しているノードは

nuke.selectedNode()

と書き表され、Blurノードを選択した状態で

sn = nuke.selectedNode()
sn.knob('size').setValue(10)

とやってみると・・・


となり、選択していた「Blur1」ノードのサイズが変更できた。

もちろん

nuke.selectedNode().knob('size').setValue(0)

とすることも可能だ。

複数選択すると、選択したノードがいわゆる「タプル型」で格納されるので、全てには「setValue()」の値が反映されない。
タプル型というのは リスト のようなものを想像してもらえればわかりやすいと思う。そのリストにあげられた、何番目のノードに対してコマンドを実行するのか?ということを宣言してあげないといけない。このあたりはまた次回説明する。

補足; toNode() だとか、selectedNode() または、createNode() ってのが ある って覚えておけば、それで問題ないかと。

ツールについて

AOVを散々押してきたけど、ちょっと考えが変わりつつある。

AOVはファイル管理がとてもしやすい。反面、パスを追加だとか、このパスだけ修正というのが、面倒になる。できないってわけでなく面倒・・・
ただ、面倒か面倒でないかってのは、割と最重要項目ではある。NukeはフォルダをNodeGraphにドラッグアンドドロップしてやれば、中に入ってるものはとりあえず、何でもかんでも読もうとする。ファイル名を後ろから読んで最初の  .(ドット) を見つけるまでを拡張してみなし、さらに、その先に(後ろ読みの)数字があれば連番と判断する。

なので、1フォルダに、数種類の連番ファイルが合ってもなんら問題ない。連番の尺が異なっていても問題ない。これは本当に便利。

AEでもこうならないかなぁ・・・ ってよくよく思う。

今ウチでは、こうやって、何でもかんでも入ってるフォルダを連番ごとにサブフォルダに仕分けするツールを使って、これを何とか解消しているが・・・ まぁNuke方式が便利である。

と、脱線はしたけど、ナンデモカンデモ入ってるフォルダでもNukeは受け付けてくれるので、実はAOVでファイルをまとめなくてもいけたりする・・・ ファイルトラフィックなど考えるとAOVの方が有利でもある気もするが・・・

ただ、ソフトウェアまたがって一つのAOVにまとめることなんてできないんで、結局は、チャンネルごとのファイルをNukeに一気に読み込んで、それらを ShuffleCopy で束ねる方がイロイロと便利な面もある。
で、今はこのツールを作りたいと思っている。 ShuffleCopy を使って自動で束ねるツール。
これができれば、このアイデアでたとえば、3Dソフトウェアからレンダリングボタンを押すと自動的に、Nukeでパスのコンポジットを行ってくれるツールとか。これはあくまで、雛形となるnukescriptの構造ありきだけど・・・

でも、レンダリングボタンをポチって押して、Nukeでビューティーを再構築したコンプ画像が出れば、イロイロと便利だ。ライティングに変更を加えるべきか、たとえば、Gradeノードで修正するべきかをすぐに判断できる・・・ まぁ、単純にNukeを一緒に開いておいて、リロード掛ければいいだけかもしれないけどw

ただ、面倒か面倒じゃないかっていうのは割りと重要で・・・

2010-08-02

Nuke Mergeの説明

まことに未熟者ですが、CGWORLD.JP にて 、「Nukeプラクティカル・ガイド」 が始まりました。
この「tiraokan」でも補完補足を行っていきたいと思っております。

で、第一回目はNuke的概念というか、思考というか、それをメインにつらつらと書かせてもらったため、じゃあ実際どうやって使うのか?ってところで疑問が多々生まれるであろうと想像しております。
当然、今後、時間を掛けて実際の使用方法を紹介していくつもりではありますが、今回は、基本的なことを列挙してみます。

まず、「Merge」・・・

通常左にあるツールバーっぽい、Nodesペーンから呼び出すか

 NodeGraphペーン上で右クリックして呼び出すか

NodeGraphペーン上でtabキーを押して、直接NodeOpの名前を打ち込むか
 
AfterEffectsでいうところのレイヤーを重ねるという行為です。ちなみに、ショートカットは[m]です。

この「tiraokan」でも最初の方に説明していましたが、MergeNodeOperatorにはインプットが「A」と「B」と通常二つ確認することができます。とりあえず、2枚の画像を合成してみます。


こんな感じです。Bがバックグラウンド。Aがフォアグラウンドです。で、この場合、AmbOccの画像で、GIライクな陰影をつけたいという意図なので、フォアの乗せ方を[multiply](乗算)に変更します。
「Properties Bin」にある、この「Merge1」の[operation]をデフォルトの[over]から[multiply]に変更します。


これで、AmbOccを [multiply]で合成することができました。
viewerペーンに何も出ないという場合は、


見たいNodeOpをViewerOPにつないで下さい。


こんな感じで。 この場合「Merge1」NodeOPを選んでキーボードの「1」を押すと、ViewerOPの一番に「Merge1」の出力がコネクトされます。

また、OpenEXRを用いると、AOVとして各種レンダリングパスを画像1枚に格納できます。


その場合、たとえば、同じ画像にある[rgb]チャンネルと[AmbOcc]を合成する場合は・・・


真ん中にある●は「Dot」と呼ばれるもので、ラインの流れ(チャンネルの流れ)を
わかりやすくするために、用いたりします。この場合使わなくていけますが・・・

こんな風にひとつの画像を[Merge1]のAとBにコネクトして、


[Merge1]のPropertyで


[A channels]を[ambocc]に変更してやり(この場合この画像にあらかじめamboccのチャンネルが存在します)、


 おなじく[operation]を[multiply]に変更してやればOK。
こっちのほうが、Nuke的でスマートです!

2010-06-03

Relighting その2

その1からの続きです。
前回は標準のReLightNodeを用いて、Relightingを行いました。ただ、このRelightingの結果を直接add mixなどでのせても味気ないので、もう一味つけるためにもReflection Mappingを施したいと思います。

Reflection Mappingに関しては、以前も一度投稿してます。 PworlrdとNworldとカメラの情報さえあれば、Nukeの中でReflection Mappingができちゃいます。やり方は、OpenGLなどでは、割と有名は方法らしいのですが(OpenGLの大家の床井先生も紹介されていました)、LTEの藤田さんに、ものすごく丁寧にご教授いただきました。その内容を説明したいとおもいます。

※注意 バカ丁寧に細かく説明しているので、長ったらしい内容になってますが、ヨクヨク読めば、たいしたないようではありません。根気よく読んでもらえればと。

原理的には、ジオメトリ上の任意の点にカメラからみてどちらの方向へ反射するかで、さらに、その反射を球体マッピング的な環境で出してしまおうという感じです。
カメラからの視線を入射角として、その視線が当たる点の法線から反射角を求め、さらにそれをuv mappingに置き換えるという手法です。

まずは、 カメラの位置とPworldから、視線ベクトル(入射角)を求めます。
Pworldは、平たく言えば各ピクセルの位置情報です。当然座標なんて本来8bitの色情報である0-255に収まらないのですが、ダイナミックレンジを格納できるOpenEXRならでは利点で0-255をプラスにもマイナスにも超えて格納してしまいます。


たとえば、上記のように、 といった感じで、8bitのRGBはNuke上では0-1でなのですが、それに縛られない数値になっていることが確認できます。Pworldではそれを利用して、RGB = XYZ として位置情報が格納されています。

それを用いて、カメラ位置を "C" としPworldを "P" とし 視線ベクトルを "E" とすると

E = normalize( P - C )

で求めることができます。
"normalize" というのは、ベクトルの長さを "1" にするという処理です。
この後法線ベクトル(Nworld)を用いて反射ベクトルを求めるために必須事項です。乱暴な解釈ですが、諸々ベクトルの長さを "1" で計算してやれば、結果、反射ベクトルの長さが一定になり、さらに、そうすると、任意のピクセルでの反射ベクトルはその一定の長さを半径としたの「球面」上のドコカシロに終点があることになり、球面マッピングの手法でReflectionが求められます。間違ってたらごめんなさい・・・

で、じゃあ、肝心のどうやってそれをNuke上で表現するかですが・・・

RGBの計算を行えるように一旦カメラ位置 "C" をExpressionNodeを用いてRGBに格納します。
Expressionを用いれば、数式はある程度まとめて、NodeOperatorの数を減らすことができるのですが、今回はわかりやすさを目指して、まわりくどく細かくNodeにわけていくことにします。
で、カメラノードの名前がcamera1として、

C = (Cx,Cy,Cz) = ((Camera1.translate.x),(Camera1.translate.y),(Camera1.translate.z))

として、これをRGBに格納します。



こんな感じで。ExpressionNodeを用いています。

で、さらに、MergeExpressionNodeを用いて、"P - C" を求めます。MergeExpressionNodeを用いると2個のインプットに対してRGBの計算を各ピクセルで行うことができます。

MergeExpressionでrgbで計算をするため、一旦Pworldをrgbに変換します。

P - C = (Px,Py,Pz) - (Cx,Cy,Cz) = ( ( Px - Cx ) , ( Py - Cy ) , ( Pz - Cz ) )

となり、Nuke上ではx,y,zをR,G,Bとして扱っており、さらに、MergeExpressionではinputがAとBとあり、それぞれのベクトルは (Ar,Ag,Ab)、(Br,Bg,Bb) であらわされます。
なので、inputAをPworld、inputBをカメラ位置 "C"とすると、MergeExpression上で

P - C = ( ( Ar - Br ) , ( Ag - Bg ) , ( Ab - Bb ) )

で示せて、実際には





さらに上で求められた、"P - C" のベクトルの長さ(length)を

length = sqrt( (P - C)x*(P - C)x + (P - C)y*(P - C)y + (P - C)y*(P - C)y )

で求めます。長さはベクトルの各成分(x,y,z)の2乗の和の平方根( √ = sqrt( ) )ですので、上の式がそれそのものです。 これをExpressionNodeに落とし込むと、

length = sqrt((r*r)+(g*g)+(b*b))

となり、視線ベクトル "E" は、"P - C" の各成分を、上記で求めた自身の長さ "length" で割ったものなので、

E = normalize( P - C ) = ( r/length , g/length , b/length, ) となり、



となり、長さ "1" のベクトルである視線ベクトル "E" が求められます。

この 長さを "1" にすることを "normalize" と呼んだり、正規化と呼んだりするようです。
ちなみに、RGB値の(この場合ベクトルでのxyzの)四足混合の一つずつだけなら、ExpressionNodeを使わずにMergeNodeの "add" "minus" "multiply" "divide" でも出来ます。順序はA operation Bのようです。minusなら A-Bの順序で組むことが可能です。

さらに、視線ベクトル "E" から反射ベクトル(反射角)を求めます。ここで、反射ベクトルを "R" 、法線ベクトル(Nworld)を "N" とし、反射ベクトル "R" はベクトルの内積を用いて次の公式で与えられます。

R = E - ( 2*(E.N)*N )

"E.N" は、視線ベクトル "E" と 法線ベクトル "N"の内積です。
内積の公式をもちいて、

E.N = (Ex*Nx)+(Ey*Ny)+(Ez*Nz)

とあらわすことができます。
さらに、これをMergeExpressionNodeに落とし込みます。どちらでも結果は同じですが、inputAを視線ベクトル "E" 、inputBを法線ベクトル "N とすると、

E.N = (Ar*Br)+(Ag*Bg)+(Ab*Bb)

であわらすことができ 、都合がいいように実際にShuffleNodeでNworldをRGBに変換しておき、MergeExpressionNodeを用いて・・・



こんな感じです。で、ひとまず

R = E - ( 2*(E.N)*N )

の残り部分 " 2*(E.N)*N " 部分までを求めてみます。MergeExpressionNodeを用いて、以下のようになります。



こんな感じで、さらに

R = E - ( 2*(E.N)*N )

の残り部分、上記で求めたものを視線ベクトル "E"から引きます。
MergeExpressionNodeを用いてもできますし、mergeのoperationを"minus"でもできます。



と、結果、これが、反射ベクトル "R" となります。


ココまで説明してきたように、MergeExpressionNodeは二つのNodeに対応しており、その二つのNode(AとB)に対して計算式を与えることができます。

で、先にも説明したように、画像上のどこのピクセルをとっても、この反射ベクトルの長さは一定なので、環境を「球面」とみなして、環境反射は反射ベクトルをもって、その球面上のある点を指定できます。実際には、計算などが扱い易いように、反射ベクトルも正規化しておきます。そうすることによって、球面上では任意の点[x,y,z]を、次の式を用いて、緯度経度座標[theata phi]に置き換えることができます。

theta = acos(-R.y)
phi = atan2(R.z,R.x) + PI

北緯○○度 東経○○度を思い浮かべてもらえばわかりやすいかもです。
ベクトルを、球体の中心からみて、それらの赤道方向の角度と北極南極を結ぶ線上をあらわす角度で表しています。

ここでのR.x、R.y、R.zは反射ベクトル "R" のそれぞれ x、y、z成分を指してます。PIは円周率です。
今までと同様にExpressionNodeでこれを表わすと



こんな感じです。

さらに、この球体(環境)の座標をuv座標(column(列)とrow(行))で考えて、0-1の範囲で表すと、
赤道方向(phi)は360度分の何度か、北極南極を結ぶ線上(theta)は180度分の何度かであらわせるので、

u = phi / (2*PI)
v = theta / PI

となりますので、




こんな感じです。

で反射ベクトルから環境を参照するためのuv座標を得ることができたので、それとSTMapNodeと環境用のマッピング画像を用いて




こんな感じで、Reflection MappingをNukeで実装できます。
ちなみに、STMapNodeはuv座標から指定したマップ(ここでは環境マップ)をルックアップしてやるためのNodeです。たとえば、


この点は r,g 成分は(0.31323 , 0.57227 )なので、マップからuv座標(column(列)とrow(行))値の(0.31323 , 0.57227 )を参照してきます。


こんな感じです。

入射角に対して、反射角の評価の仕方などを法線とあらかじめ用意したUVなどをくみあわせて、うまくいじってやれば、異方向性反射なども実装できると思います。
今回は環境マッピングでしたが、考え方はshadingにちかいので、NukeではPhong shaderしかありませんが、いろいろとカスタマイズすることも可能です。

もう少し続きますが、今回はココまで。