2011-12-09

ToolSets と python の組み合わせ その2

と、前回は基本的なpythonコードをnukeから実行するという説明でした。しかしながら、別にファイルコピーが出来たからってルックがあがるわけでもなく、アーティストレベルだとそんなに要らないんじゃないか?そんな効率が上がったところでトラッキングがぴったり合うわけでも、クロマキーで撮った素材のなじみがグッと良くなるわけでもないかもです。
ただ、トライアンドエラーの回数が増えれば自ずとクオリティーはあげていけるんじゃないかと。

とあるアニメーションがすごく上手な方に訊いてみました・・・


半ば誘導尋問ではありますが、上手い人でも何度も確認してっていう工程があると思います。経験をつめばある程度その部分が早くなっていくんでしょうが。

で、話はnukeに戻して、nukeの問題点、そのうちの一つはやはりプレビューにあると思います。再生ボタンおして aggressive cache をオンにしていても、fpsがまともに出なくて、やはり引っかかる部分もあるし、AfterEffectsのRAMプレビューのような快適さと賢さは備わっていないと思います。ぜんぜん aggressive じゃありません。

それを回避するために、6.2ぐらいから、「Flipbook this viewer」という機能が標準で付きました。


結果を見たいviewerを選んで実行すると、レンダリングが始まりレンダリング終了後にFrameCyclerが立ち上がります。FrameCyclerはRAMにキャッシュさせて再生が可能なので、それだと指定したfpsをしっかりと実現して確実なプレビューができます。
こういったものを使えば、一々Writeノードでファイルパスなどをつけたり、ディレクトリを切ったりとという煩わしさも無くプレビュー・確認の回数を気軽に増やすことが可能です。
ただ、FrameCyclerが重いし使い辛い。しかも、nukeに標準でついてくるFrameCyclerはどうやらnukeと連動してでしか使えないようで、単体で立ち上げようとしてもライセンスエラーなどで立ち上がりません。
この「Flipbook this viewer」、実は、Flipbookの部分でFrameCyclerしか選べないんですが、pyをいじることでPDPlayerのような別のシーケンスビューワーを選ぶことも可能といえば可能です。このただ「Flipbook this viewer」のインターフェイスを用いてやるには諸々と改良が必要ですが・・・

と前置きがまたもやかなり長くはなりましたが、ToolSets と python を組み合わせて使うことで自前でプレビューを簡単に作る仕組みを作ることを今回の目的としています。
流れとしては・・・

1.選んだノードに新規でWriteノードをぶら下げる。
2.そのWriteノードはQuickTimeをレンダリングするようにしておき
3.勝手にレンダリングを行い、終了後そのQuickTimeを自動的に開く

ということをしようと思います。これで選んだノードでストレス無くプレビューを作れ、かつバージョン保存等も可能です。
1.の 選んだノードに新規でWriteノードをぶら下げる ですが、コレは、以前の「ToolSets と python の組み合わせ その1」ほぼ同じです。なんですが、今回はToolSetsを利用します。どういうことかというと、QuickTimeなどの面倒な設定をpythonから呼び出すのではなくToolSetsとしてテンプレートを記録しておきます。AfterEffectsのレンダリング設定のテンプレートのようなものです。 

まずはテンプレートとなるノード群を作成します。とりあえずシンプルにWriteノードで出力先を指定せずにQuickTimeのコーデックの種類等を決定します。


ファイル名(ファイルパス)は後ほど設定します。
さらにフレーム番号を右下に表示させたいので、Textノードを追加して、


で、このTextノードは


というような感じで、諸々が設定できるので、
まず、文字のサイズ。HDサイズで「75」ポイントとして、そのほかのサイズがきても、ちょうどいいサイズになるように、


75 * float(nuke.value(nuke.thisNode().input(0).name() + ".bbox.h")) / 1080

エクスプレッションとしてpythonコードをいれた。
細かく見ると、

nuke.thisNode().input(0).name() でこのノードの親ノードの名前
nuke.value(nuke.thisNode().input(0).name() + ".bbox.h") で上記の親ノードの名前の縦の解像度

floatをつけているのは、どうやら nuke.value(nuke.thisNode().input(0).name() + ".bbox.h") が文字列で帰ってくるみたいなので、floatをつけて数字として扱えるようにしている。
あとはその縦の解像度を 1080 で割ってHDサイズとの比率をだして、最後に75を掛けている。

とそんな感じで、どんな解像度がきても同じ位置に、解像度に対して同じようなサイズでフレーム数が入るようにした。

で、この二つのノードをToolSetsとして登録する。


二つのノードを選択して、「ToolSets」→「Create」で


適当な名前をつけて保存する。
以前にも説明したが、一度登録すると、ホームディレクトリの「 .nuke」フォルダに先ほどつけた名前の.nkで保存され、「ToolSets」から呼び出せるようになる。

 
こんな感じで。
あとは、このToolSetsをpythonから呼び出し、レンダリングされるQuickTimeのファイルパスに与えてやり、それをショートカットに割り当てれば大まかに完成だ。(勝手にレンダリングを始めるようにしたり、プロキシーモードで実行したり、レンダリング後勝手にQuickTimeを開いたり・・・ 色々とあるが)

ということで、コレを呼び出すというのをpythonで表現すると・・・

--------------------------------------------------------
nuke.loadToolset(.nkのファイルパス)
--------------------------------------------------------

になる。.nkは先ほども書いたがホームディレクトリの「 .nuke」フォルダに保存されてる。
ホームディレクトリは

C:\Users\hogehoge\.nuke\

のように直接的に明示することも出来るが、せっかくpythonを用いるのだから環境に依存しないように、ある程度どこで開いても可能なように書きたいので、

-------------------------------------------------------- 
import os 

os.environ['HOME']
-------------------------------------------------------- 


でホームディレクトリを表せることをもちいて、

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

import os
mynukePath = os.environ['HOME'] + r"/.nuke"
nuke.loadToolset(mynukePath + r"/ToolSets/Preview.nk")

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

で、いま登録したToolSetsが呼び出せる。

呼び出した後は、それで作成されたノードが選択されているので、それを利用して、Writeノードのファイルパスを設定する。

-------------------------------------------------------- 
nuke.selectedNodes('Write') 
-------------------------------------------------------- 

とすると、選択しているノード群の中のWriteノードを表すことが出来ます。ただし、  

nuke.selectedNodes


と、たとえ表現したいWriteノードが一つでも「s」をつける必要があり、「s」が付いているので、返ってくるのはリストです。今回の場合は呼び出したToolSetsにWriteノードは一つしかないので、このToolSetsを呼び出した直後だと

-------------------------------------------------------- 
nuke.selectedNodes('Write')[0]
--------------------------------------------------------

となります。
でファイルパスを指定するのですが、今回は簡単にするために.nkと同じところに.nkと同じ名前で保存することにします。
(以前のエントリー 「関数と引数 その1 (python入門)のコメントでtakkunさんに教えていただいたos.path.dirname などを使えばもっと色々と出来ます。

.nkのファイルパスは

--------------------------------------------------------
nuke.root().name()

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

なので、

--------------------------------------------------------
wrNode = nuke.selectedNodes('Write')[0]
nkName = nuke.root().name()
qtName = nkName.replace(".nk", ".mov") 
qtProxy = nkName.replace(".nk", "_proxy.mov")
wrNode.knob('file').setValue(qtName)
wrNode.knob('proxy').setValue(qtProxy)
 --------------------------------------------------------

となります。プロキシーにも自動的に_proxyで設定するようにしました。
 あと、レンダリングのフレームレンジですが、シーンと同じ尺として、

--------------------------------------------------------
fframe = nuke.root().knob('first_frame').value()
lframe = nuke.root().knob('last_frame').value()
wrNode.knob('first').setValue(fframe)
wrNode.knob('last').setValue(lframe)
--------------------------------------------------------

です。
で、コレを実行するには

--------------------------------------------------------
nuke.execute(wrNode, fframe, lframe)
--------------------------------------------------------

です。さらに、レンダリング後にこのQuickTimeが自動的に立ち上がるようにするために

-------------------------------------------------------- 
import os
os.popen(qtName)
--------------------------------------------------------


とし、レンダリング結果のファイルパス(qtName)をpopenから実行することでそのQuickTimeを開くようにしました。



またレンダリング終了後このノードを消したい場合は、

--------------------------------------------------------
for n in nuke.selectedNodes():
    nuke.delete(n)

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

です。
これらをまとめると、

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

import os

#ToolSetsの呼び出し
mynukePath = os.environ['HOME'] + r"/.nuke"
nuke.loadToolset(mynukePath + r"/ToolSets/Preview.nk")

#ToolSetsのWriteノードの各種設定
wrNode = nuke.selectedNodes('Write')[0]
nkName = nuke.root().name()
qtName = nkName.replace(".nk", ".mov") 
qtProxy = nkName.replace(".nk", "_proxy.mov")
wrNode.knob('file').setValue(qtName)
wrNode.knob('proxy').setValue(qtProxy)
fframe = nuke.root().knob('first_frame').value()
lframe = nuke.root().knob('last_frame').value()
wrNode.knob('first').setValue(fframe)
wrNode.knob('last').setValue(lframe)

#レンダリング
nuke.execute(wrNode, fframe, lframe)



#レンダリング後のQuickTimeの実行
os.popen(qtName)

#レンダリング後のノードの削除
for n in nuke.selectedNodes():
    nuke.delete(n)

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


となります。これをショートカットに割り当てて容易に呼び出せるようにするために関数かします。といっても引数は無いのでそんなに難しくはありません。

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

import nuke
import os

def previewQT():
    #ToolSetsの呼び出し
    mynukePath = os.environ['HOME'] + r"/.nuke"
    nuke.loadToolset(mynukePath + r"/ToolSets/Preview.nk")

    #ToolSetsのWriteノードの各種設定
    wrNode = nuke.selectedNodes('Write')[0]
    nkName = nuke.root().name()
    qtName = nkName.replace(".nk", ".mov") 
    qtProxy = nkName.replace(".nk", "_proxy.mov")
    wrNode.knob('file').setValue(qtName)
    wrNode.knob('proxy').setValue(qtProxy)
    fframe = nuke.root().knob('first_frame').value()
   
lframe = nuke.root().knob('last_frame').value()
    wrNode.knob('first').setValue(fframe)
    wrNode.knob('last').setValue(lframe)

    #レンダリング
    nuke.execute(wrNode, fframe, lframe)



    #レンダリング後のQuickTimeの実行
    os.popen(qtName)


    #レンダリング後のノードの削除
    for n in nuke.selectedNodes():
        nuke.delete(n)

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

とし、previewQT.pyという名前で.nuke フォルダ に保存して、previewQT() で呼び出せるようにしました。

あとは、.nuke フォルダにある menu.py に


--------------------------------------------------------
import previewQT
m.addCommand( "previewQT", "previewQT.previewQT()", "Alt+r")
--------------------------------------------------------

と記述を加えると、 「Alt+r」で呼び出せます。
最初の 「previewQT」はコマンド名なので、適当につけて問題ないです。
次の「previewQT.previewQT()」はあらかじめ「import previewQT」でインポートしたpreviewQT.pyの関数previewQT()ということになります。

これで、previewしたいノードを選んでショートカットを押せば自動的にレンダリングされ、プレビューとなります。
 
もう少しカスタマイズすれば、プロキシーでレンダリングしたり、1フレ飛ばしでレンダリングしたりと色々と可能です。

2011-12-05

関数と引数 その1(python入門)

前回、「ToolSets と python の組み合わせ その1」の続きを書こうといきまいたのですが、どうやっても関数と引数の説明を避けて通れないので、今回は関数と引数の説明を。

自分でも、シッカリとプログラムを勉強したわけでもないですし、授業等で習ったわけでもないので、全て独学です。なので上手く説明できるか・・・

ともかく関数というのは 、何かを実行するための「機能」です。たとえば今回まずやりたいのは、ファイルのコピーとします。ファイルのコピーには最低でも

「どこにある何て名前のファイルを(コピー元ファイルパス)」を
「どこ(コピー先ファイルパス)」へコピーするのか。

この二つのことを、この「機能」を伝えてやる必要があります。
このある「機能」にたいして「最低限伝えなければならないこと」を引数といいます。「機能」そのものが関数になります。

関数は

def hoge(引数):
    実行文   (※hogeは適当にかいてます。)

という形で定義されます。
「def」 に続けて 関数名+()、でその後に:(コロン)です。
実行文はコロンの後に改行してtab(インデント)や複数の半角スペースで段差をつけます。この段差でこの関数がココからココまでと宣言しているので段差は重要です。

実際に書いてみます。
-------------------------------------------------------
def hoge():
    print "Hello!"
-------------------------------------------------------
実行する場合は、hoge() で実行します。
print はスクリプトエディタのアウトプット側に指定されたことを表示するってことです。
なので、この関数は実行すると Hello! と表示するスクリプトということになります。
また表示したいことは Hello! そのもので文字列(文字そのもの)なので、" " で表示したい文字列を囲っています。


上のアウトプット側には Hello! が出ていますが、" " は出ていません。
たとえば、

-------------------------------------------------------
A = "Hello!"
def hoge():
    print A
-------------------------------------------------------

として、hoge() を実行すると、Aが表示されず A = "Hello!" で定義された、Hello! が表示されます。


ここでもやはり、" " がなく Hello! と表示されます。

さっきは、A = "Hello!" であらかじめ A を定義しましたが、関数の魅力は後から変数を指定できるということでしょうか。つまり、

何を表示するかは未定だけど、とりあえず、あとから決まった事を表示する

といったところです。
具体的には

-------------------------------------------------------
def hoge(A):
    print A
-------------------------------------------------------

とします。何をprintするかは決まってないけどとりあえず A としておいて、関数として hoge(A) で、とりあえずprintすることは A で仮に置いておくけど、あとからちゃんと決めてね という感じです。

で hoge() をすると


となってなにやらエラーが出ます。
とりあえず、A で仮置きしたけど、その A って何よ? って感じのエラーでしょうか。
つまり、表示する A にあたるものを宣言してやる必要があります。
それは hoge() の ()カッコ中に書きます。
たとえば hoge("Good Bye!") とすると


結果として Good Bye! が表示されました。
コレが引数です。あらかじめ、関数内で後に宣言するであろう事を仮置きしておき、hoge(引数) とすることで、関数にその引数が渡されます。
この引数は一つに限られません。たとえば、

-------------------------------------------------------
def hoge(A, B):
    print A + " and " + B
-------------------------------------------------------
とします。引数としてAとBの二つを与えると、結果として A + " and " + B を返すということですが、この + は文字を足すってことです。
ここで、

hoge("johnson", "johnson")

とすると・・・


となり johnson and johnson を得ます。 hoge("Carl", "Lewis") とすると、


Carl and Lewis を結果として得ます。

と前置きが長かったですが、要するに コピー元のファイルパス と コピー先のファイルパス を引数とするコピーするための関数をつくろうって事です。
レンダリング結果を指定した場所にコピーする というものを作ろうと思います。

nukeでは連番を扱うことが多いので、ファイル単体をコピーというよりはフォルダごとのコピーを紹介します。
で、pythonでその「フォルダごとコピー」をするには copytree() という関数を用います。copytree() を実行するには shutil というライブラリ(モジュール)を読み込む必要があり、そういう場合は

import shutil

と最初に宣言すれば shutil 内で定義されてる関数を用いることが出来ます。実は、これは標準ですでに存在する .py なのですが、どうやら、プログラムというものは全てのライブラリ(モジュール)を一旦読み込んできて、それで実行コードを展開していくというものではなく、必要なライブラリだけ必要に応じて読んできて・・・ っていうものであるらしいです。


こんな感じで、pythonのインストールされたフォルダの Lib フォルダに shutil.py は保存されています。どうやら、pythonはディフォルトでこの Lib フォルダの中身を読む準備が出来ているようです。いわゆる環境パスってヤツでしょうか。
でこの shutil.py の中身ですが・・・


こんな感じで、def ~~(): で定義された関数が複数個収録されてます。実際は import shutil をして、続きは

-------------------------------------------------------
import shutil
shutil.copytree(src, dst)
-------------------------------------------------------

と書きます。そうすると src(ファイルパス) で指定されたファイルが dst(ファイルパス) にコピーされます。
pythonのディレクトリのLibに保存されている(もしくはpythonがデフォルトで読みにいけるディレクトリに保存されている) shutil.py の中で定義されてる、copytree() という関数に、引数として、コピー元としてsrc(ファイルパス)と、コピー先としてdst(ファイルパス)を与える ということです。

そこで、たとえばnuke内から

-------------------------------------------------------
import shutil
shutil.copytree(r'C:/render/111205', r'//192.168.1.5/z/works/hoge/render/111205_copy')
-------------------------------------------------------

を実行してみます。


すると、


指定したディレクトリにファイルがコピーされました。(WindowsのインターフェイスがXPになったり、Win7になったりなのは、このブログを家で書いたり、会社で書いたりしているせいです・・・)

でたとえば、コレをつかって、Writeノード で自動的にレンダリング結果を指定したディレクトリにコピーするという関数を作ってみたいと思います。

この関数はWriteノードから実行するので、レンダリング結果のファイルパスは

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

であらわせます。

-------------------------------------------------------
import shutil

def rendercopy(dst):
    shutil.copytree(nuke.thisNode().knob('file').value(), dst)
-------------------------------------------------------
※dstは引数

であらわせます。これを .nuke((ドット)nuke) フォルダにたとえば mytool.py として保存します。nukeは起動時に .nukeフォルダにある .py を読み込んでいます。なのでWriteノードの「after render」には

-------------------------------------------------------
 import mytool;mytool.rendercopy( コピー先のディレクトリ )
-------------------------------------------------------

とします。実際には以下のような感じです。


本来ならば

-------------------------------------------------------
import mytool
mytool.rendercopy(r'//192.168.1.5/z/works/hoge/render/111205_copy')
-------------------------------------------------------

です。 ただ、このWriteノードの「after render」では改行がきかないので改行部分に ;(セミコロン) を入れてます。

menu.py であらかじめこの.pyをインポートして関数を使えるようにしておけば、最初の import hoge のくだりは不要で、

-------------------------------------------------------
mytool.rendercopy( コピー先のディレクトリ )
-------------------------------------------------------

だけの記述で動きます。
ただ、今回、連番でレンダリングの出力先が


というようになっていて現状の、rendercopy() で取得する、nuke.thisNode().knob('file').value() だと返ってくるファイルパスが 「C:/render/test_%04d.jpg」 となります。
shutil.copytree() ではフォルダを指定しないといけないので、実際には「C:/render」をshutil.copytree()の一個目の引数として渡してやる必要があります。
なので、 「C:/render/test_%04d.jpg」 を 「C:/render」に変えてやる必要があります。
そこで、
-------------------------------------------------------
import re

src = nuke.thisNode().knob('file').value()
srcSplit = src .split('/')
src = src.replace(srcSplit[-1], "")
-------------------------------------------------------

とします。reモジュールの中のreplace()という関数を使ってます。

replaceは
-------------------------------------------------------
hoge = "I want a pineapple"
hoge = hoge.replace("a pineapple", "an apple")
-------------------------------------------------------

とすると hoge = "I want a pineapple" で定義された hoge を print hoge をすると


replace(src, dst) によって src を dst に置き換えられたということになります。
 なので、srcでファイルパスを定義したら、最後の「test_%04d.jpg」を取り除くために、

-------------------------------------------------------
src = nuke.thisNode().knob('file').value()
srcSplit = src .split('/')
-------------------------------------------------------

としてやって /(スラッシュ) ごとに文字列を分けていってリスト化します。
そのリストの最後が「test_%04d.jpg」となるはずなので、それは、

-------------------------------------------------------
src = nuke.thisNode().knob('file').value()srcSplit = src .split('/')
print srcSplit[-1]
-------------------------------------------------------


とすると、「test_%04d.jpg」が返ってくるはずです。
なので、ここで、さっき説明したreplace()を用いて、入れ替える文字列を無し("")にしてやると

-------------------------------------------------------
import re


src = nuke.thisNode().knob('file').value()
srcSplit = src .split('/')
src = src.replace(srcSplit[-1], "")
-------------------------------------------------------

とすれば、当初  「C:/render/test_%04d.jpg」 だった src が 「C:/render/」 に変わります。

なので、mytool.pyを

-------------------------------------------------------
import nuke
import shutil
import re

def rendercopy(dst):
    src = nuke.thisNode().knob('file').value()
    srcSplit = src .split('/')
    src = src.replace(srcSplit[-1], "")
    shutil.copytree(src, dst)
-------------------------------------------------------

と修正してセーブしてnukeを立ち上げなおしてWriteノードを実行させます。

そうすると、レンダリング終了後、このWriteノードの「after render」で指定した
-------------------------------------------------------
 import mytool;mytool.rendercopy( r'//192.168.1.5/z/works/hoge/render/111205_copy' )
-------------------------------------------------------

//192.168.1.5/z/works/hoge/render/111205_copy

に 中身がコピーされていました。

とこんな感じで、nuke内のパラメーター、もしくはユーザーの指定のパラメーターを引数として自分のやりたい機能を関数化をしてやるとWriteノードのpythonタブから用いることが出来ます。

レンダリング終了後にメールを送るとかもできますね。(特にいらないけど・・・)