2014年02月

2014年02月28日

2014の新機能を調べてみた その86 3dsmax 2014

前回に引き続き「DemoMaterials.py」を調べてみたい。

前回書いた通り回転はクォータニオン値にしてからノードにセットする。

angle_rotate = 180-i
angle_axis_rotation = MaxPlus.AngAxis(0,0,1, math.radians(angle_rotate))
quat = MaxPlus.Quat(angle_axis_rotation)
node.SetLocalRotation( quat )

回転は「WorldRotation」と「LocalRotation」があって、この値にアクセスするのに上のコードのように「Set〜」や「Get〜」のファンクションを使う方式と、属性値として代入したり参照したりする方式がある。どちらも同じ意味になる。

node.SetLocalRotation(quat)
node.LocalRotation = quat

quat = node.GetLocalRotation()
quat = node.LocalRotation

node.SetWorldRotation(quat)
node.Rotation = quat

quat = node.GetWorldRotation()
quat = node.Rotation

ただ、属性値として代入する場合、名前のチェックが無いみたいで、間違って、

node.rotation = quat

とやってもエラーは出ない。もちろん回転もしないので注意が必要なようだ。

ところで上記の通り回転には「WorldRotation」と「LocalRotation」があるんだけど、この違いはちょっとわかり辛い。名前からして「WorldRotation」の方はワールド座標を基準に回転して、「LocalRotation」の方はローカル座標を基準に回転しそうなもんだけど、実際はノードが持っているワールド変換マトリクスをどう加工するかって事らしい。

変換マトリクスって言うのは高校の授業でやった行列のことで、3次元座標に3X3の行列をかけると他の座標に変換できたりしたあの行列のことだ。MAXでは1つの行列で移動、回転、スケールの変換が同時に行えるように4X3の行列(matrix3型)が採用されている。この行列の便利なところは、移動や回転、スケールを表わす個々の行列は単純な形であらわせて、しかもその変換を組み合わせた変換はそれらの行列の積として作ることが出来る事だ。だから何段階にもペアレントされた複雑な構造のノードでも、おおもとのノードの変換行列から順に掛け算していけば、末端のノードの変換行列が簡単に算出できる。実はmaxのノードはこの各ノードの変換行列を乗じた結果をワールド変換行列として持っている。だからあるノードを他のノードにペアレントしようとした場合、親となるノードのワールド変換行列を取得して来て子の方の移動、回転、スケールの行列をかけてやれば即座にそのノードのワールド変換行列が求まるってわけだ。この行列にさらにオフセットとかが作用したものをオブジェクトの頂点のローカル座標にかけてやれば、シーン上の頂点の位置が決まって、シーンにオブジェクトが出てくるわけだ。

ちなみに回転とスケールの変換行列は4X3の行列の左側の3列を使い、移動は右の1列を使って表現される。だから回転とスケールはごっちゃになるけど移動は4列目を見ればワールド座標がすぐにわかる。

ノードのワールド変換行列を「NodeWorldTM」、ノードのローカル変換行列を「NodeLocalTM」、ペアレントしている親のワールド変換行列を「ParentWorldTM」とすると、以下の式が成り立つ。

NodeWorldTM = NodeLocalTM * ParentWorldTM

で、実はmaxでは、各ノードにはワールド変換行列は記録してあるけど、ローカル変換行列は存在しない。だからローカル変換行列が欲しい時は自分のワールド変換行列と親のノードのワールド変換行列から逆算して出すようになっているらしい(もちろんペアレントしていなければ、ローカルもワールドも同じ変換行列になる)。上式を変形して「NodeLocalTM」の式にすると、以下のようになる。「Inverse()」は逆行列を表わしているよ。

NodeLocalTM = NodeWorldTM * Inverse(ParentWorldTM)

さらに「NodeLocalTM」は前述の通り移動と回転とスケールの行列が乗算されたものだ。

で、話が戻って「WorldRotation」と「LocalRotation」だ。どうやらこの2つの違いは、「NodeWorldTM」に対して回転するのか、「NodeLocalTM」に対して回転するのかの違いのようだ。しかも回転は3X3行列の掛け算で行われるようで、移動の項は入ってこないので、回転はオフセットが無ければオブジェクトのローカル中心で行われる。

その結果として「WorldRotation」の回転は親ノードのスケールが含まれた状態のワールド変換行列を回転させるので、そのノードのローカルスケールに親ノードのスケールがそのまま入ってしまい、さらにそのノードに親ノードのスケールが作用するのでスケールが2度作用することになる。一方「LocalRotation」は親のノードの影響がキャンセルされるので、ローカルスケールには影響が無く、親ノードのスケールだけが作用する。

例えば下のシーンのように、BOX2つを球にペアレントして、球のZ軸スケールを50%にした場合、2つのBOXもZ軸スケールが50%になる。

fig01

ここで、以下のプログラムを実行して2つのBOXを45°回転させてみると、

import MaxPlus
import math

r1 = MaxPlus.Core.EvalMAXScript('$Box001')
r2 = MaxPlus.Core.EvalMAXScript('$Box002')
n1=r1.GetNode()
n2=r2.GetNode()
aa = MaxPlus.AngAxis(0,0,1,math.radians(45))
q = MaxPlus.Quat(aa)
n1.SetWorldRotation(q)
n2.SetLocalRotation(q)

回転自体はノードの基点を中心に行われ、ワールド回転の方はスケールが25%になった(Box001のスケール自体は50%で、球のスケールが50%になっている)。

fig02

なんだかややこしいけど、このへんをよく調べておかないと、いろいろ混乱しそうだ。

それではまた来週。

maxまとめページ



take_z_ultima at 11:30|この記事のURLComments(0)TrackBack(0)3ds Max | CG

2014年02月27日

ACSを使ってみた その56 modo701 SP2

SP4出てるのかよ・・・orz

もうSP出ても更新のお知らせ出なくなったのかな?自分のmodoはSP2で更新チェックすると

fig07

って出るんだけど・・・。

という事はこのスクリプトはまさに徒労では・・・。直ってて欲しいような欲しくないような・・・w

気を取り直してとりあえず続きを・・・。

前回に引き続き選択アイテムのポーズを登録するスクリプトについてコードの主要部分について書いておくよ。

「copySelPose()」は選択アイテムのポーズをファイルに書き出すファンクションだ。

まずファイルを書き出しモードで開く。ファイルは作業フォルダに「PoseCopyBuffer.txt」という名前で作られる。ホントはファイル開く前にやった方が良かったんだろうけど、次に選択されているアイテムの数を調べて1つも無い時は「valueError」例外を発生してこのファンクションを抜ける。

def copySelPose():
 f = open(BufferFileName, 'w')
 if len(selItems)==0:
 raise ValueError,"コントローラを選択してください"

このファンクションはメインルーチンの「try〜exception」の中で呼び出されているので、例外はそこに渡されて、

except ValueError,message:
 lx.eval('dialog.setup error')
 lx.eval('dialog.title "Error"')
 lx.eval('dialog.msg "%s"' % message)
 lx.eval('dialog.open')

の部分にキャッチされてエラーダイアログに「コントローラを選択してください」と表示されてプログラムを終了する。

fig01

選択アイテムがあった場合、それらを変数「item」にひとつずつ取り出してfor文で巡回する。

for item in selItems:

コントローラの有効なチャンネルはアイテムの「RGCH」タグを見ればわかる仕様になっているようなので、まずそのアイテムの全タグを取得して、その中に「RGCH」タグがあるかどうかを調べて、あったらその値を取得する。

tagtypes = lx.evalN('query sceneservice item.tagTypes ? %s' % item)
if "RGCH" in tagtypes:
lx.eval('select.item %s mode:set' % item)
rgch = lx.eval('item.tag mode:string tag:"RGCH" value:?')

RGCHタグはこんな感じで「p」「r」「s」「u」「#」「=」が並んでいて、これらが有効なチャンネルを表わしているみたいだ。

fig02

アルファベットはp:位置、r:回転、s:スケール、u:ユーザーチャンネルをそれぞれ表わしていて、「#」は有効で「=」はアニメーションとしては有効じゃないチャンネルのようだ。p、r、sの後ろに3つずつ並んでいるのはそれぞれxyzの3チャンネル持っているためだろう。

そこで、まず取得したタグ値から「p」を見つける。見つからなければpposには−1が入る。それをチェックして、−1じゃなければ、pの位置の次の文字を調べて「#」なら位置のXチャンネルが有効なので、そのチャンネルの値を調べてそれをファイルに書き出す。

ppos=rgch.find("p")
if ppos !=-1:
 if rgch[ppos+1]=='#':
  px=lx.eval('item.channel pos.X ?')
  f.write("%s pos.X %s\n" % (item,px))

ファイルに書き出すデータのフォーマットは、

アイテムID チャンネル名 値

この1行のデータでチャンネルが確実に特定できるので、値を戻すのも簡単だ。残りのY、Zチャンネルも同様に処理する。

if rgch[ppos+2]=='#':
 py=lx.eval('item.channel pos.Y ?')
 f.write("%s pos.Y %s\n" % (item,py))
if rgch[ppos+3]=='#':
 pz=lx.eval('item.channel pos.Z ?')
 f.write("%s pos.Z %s\n" % (item,pz))

さらに「r」「s」も同様に処理する。

rpos=rgch.find("r")
if rpos !=-1:
 if rgch[rpos+1]=='#':
  rx=lx.eval('item.channel rot.X ?')
  f.write("%s rot.X %s\n" % (item,rx))
 if rgch[rpos+2]=='#':
  ry=lx.eval('item.channel rot.Y ?')
  f.write("%s rot.Y %s\n" % (item,ry))
 if rgch[rpos+3]=='#':
  rz=lx.eval('item.channel rot.Z ?')
  f.write("%s rot.Z %s\n" % (item,rz))

spos=rgch.find("s")
if spos !=-1:
 if rgch[spos+1]=='#':
  sx=lx.eval('item.channel scl.X ?')
  f.write("%s scl.X %s\n" % (item,sx))
 if rgch[spos+2]=='#':
  sy=lx.eval('item.channel scl.Y ?')
f.write("%s scl.Y %s\n" % (item,sy))
if rgch[spos+3]=='#':
sz=lx.eval('item.channel scl.Z ?')
f.write("%s scl.Z %s\n" % (item,sz))

ユーザーチャンネルはチャンネルを一部ラベルとして使っているのでちょっとややこしい。下の画像は手の設定チャンネルだ。この中の「x..............Controls」とか「v........................Settings」とかがラベルとして使っているチャンネルだ。これらはパネルにわかりやすいラベルを表示するために追加されたチャンネルで値に意味は無い。

fig05

だからタグの方はこのラベルを無視した形で「#」と「=」が並んでいる。上の画像に表示されているユーザーチャンネルは16個(うち3個がラベル用)だけど、下のタグの「u」の項目は「#」と「=」の合計が13個だ。ラベルのチャンネルの数を引くと丁度合致する。

fig06

実際のチャンネルはこんな感じ。ロケータに設定されたチャンネルの一部分としてユーザーチャンネルが存在する。

fig04

これもどこまでがロケーターのチャンネルで、どこからがユーザチャンネルなのかを調べる方法がわからなかったので、ロケータを個別に調べて61番目からがユーザーチャンネルになってるようだという結論になったので、最初のラベルをスキップして62番目から残り全てのチャンネルを順にファイルに書き出して記録する事にした。このへんはちょっと手抜きだな。

upos=rgch.find("u")
if upos!=-1:
 lx.eval('query sceneservice item.name ? %s' % item)
 n=lx.eval('query sceneservice channel.N ?')
 for i in range(62,n):
  cname = lx.eval('query sceneservice channel.name ? %s' % i)
  cvalue = lx.eval('query sceneservice channel.value ? %s' % i)
  f.write("%s %s %s\n" % (item,cname,cvalue))

全部書き出したらファイルを閉じてファンクションの終了だ。

f.close()

「pasteSelPose」ファンクションはファイルに書き出したポーズを復元するためのものだ。ファイルを読み込み用として開いて、1行読んではそれを分析して、アイテム名、チャンネル名、値に戻して、そのアイテムを選択して、そのアイテムのチャンネルに値をセットしている。

def pasteSelPose():
 f = open(BufferFileName, 'r')
 line = f.readline()
 while line:
  data = line.strip('\n').split(' ')
  if len(data)==3:
   lx.eval('select.item %s mode:set' % data[0])
   lx.eval('item.channel %s %s' % (data[1],data[2]))
   line = f.readline()
 f.close()

1行読み込むと、行の最後に改行の文字コードが入るので、「strip」メソッドでこれを取り除いて、その文字列を「split」メソッドを使って「スペース」1文字を区切文字として分解してリストにする。データ1行は

アイテムID チャンネル名 値¥n

となっているので、変数dataは、

(’アイテムID’,’チャンネル名’,’値’)

というリスト(タプル)になる。あとは添字をつければ個別に呼び出せるので、アイテムIDなら「data[0]」、チャンネル名なら「data[1]」で呼び出して、アイテムを選択したりチャンネルに値をセットするのに使った。

全ての行が無くなったらファイルを閉じて終了だ。

以上がこのプログラムの中身だ。ACS以外で使う時はRGCHのタグとか無いからチェックはやめてそのまま位置、回転、スケールのチャンネルを全部コピペするようにしちゃえばいいと思うよ。

それではまた次回。

modo701ブログ目次



take_z_ultima at 11:30|この記事のURLComments(0)TrackBack(0)modo | CG

2014年02月26日

2014の新機能を調べてみた その85 3dsmax 2014

前回に引き続き「DemoMaterials.py」を調べてみたい。

「CreateAndAssignMaterials」は全てのマテリアルオブジェクトが入ったリストを引数で受け取って実行が始まる。まずはそのリストに入っているマテリアルの数を「len」で調べて変数「numMaterials」にセットする。

def CreateAndAssignMaterials(materials):
 numMaterials = len(materials)
 diff = 360.0 / numMaterials

そしての数で360を割った値を変数「diff」にセットする。このプログラムは下のように全てのマテリアルのサンプルを円周上に等間隔で並べるようにしているので、「diff」はそのための中心角の1ステップぶんの角度になる。

fig07

次にファンクション内で使う変数の初期化をして、

teapot_radius = 5.0
radius = 50.0
text_radius = 90.0
index = 0
i = 0
cancel = 0

マテリアルエディタをコンパクトモードで開く。

MaxPlus.MaterialEditor.OpenMtlDlg(MtlDlgMode.basic)

fig02

マテリアルエディタのモードは引数が0ならコンパクトモード、1ならストレートモードになるようだ。Pythonの文法には定数が無いのでこのプログラムでは他の定数同様にクラスでまとめて表現している。

class MtlDlgMode(object):
 ''' Enumeration that determines what kind of material dialog to display'''
 basic = 0 # Basic mode, basic parameter editing of material and textures
 advanced = 1 # Advanced mode, schematic graph editing of material and texture connections

だから「MtlDlgMode.basic」は0になって、

MaxPlus.MaterialEditor.OpenMtlDlg(0)

でコンパクトマテリアルエディタが出て来るわけだ。マニュアルには

OpenMtlDlg(int mode)

しか書いてないので、「mode」の部分に何の数値を入れるもんやら、こういう例でも無いと困るよなぁ。せめてこういう定数は「MaterialEditor」クラスで定義しておいて欲しいよね。

ここから先はfor文のループで、変数「m」に「materials」に入ったマテリアルオブジェクトを1つずつ取り出して処理するのを繰り返す事になる。

for m in materials:

まずはティーポットと文字を配置するための位置を変数「position」にPoint3クラスのオブジェクトとして取り出す。

angle_radians = math.radians(i)
x = radius * math.cos(angle_radians)
y = radius * math.sin(angle_radians)
position = MaxPlus.Point3(x,y,0)

変数「i」が円の中心角で、それをラジアン単位に変換してから三角関数で半径radiusの円周上の座標を計算している。

fig03

そして「GeomObject」としてティーポットを生成して、ティーポットの半径を「teapot_radius(中身は5)」に設定して、ノードに割り当ててビューポートに出し、先に作成した「position」をノードの「Position」プロパティに割り当てて位置決めする。

  teapot = MaxPlus.Factory.CreateGeomObject(MaxPlus.ClassIds.Teapot)
  teapot.ParameterBlock.Radius.Value = teapot_radius
  node = MaxPlus.Factory.CreateNode(teapot)
  node.Position = position

ティーポットの向きは全て並べる円の中心方向に注ぎ口が向けるように調整される。

fig05

ティーポットの注ぎ口はローカル回転角度0度でX軸正方向を向いている。今、中心角をiとすれば、ティーポットはZ軸まわりに(180−i)度または(180+i)度回転させればいことになる。

回転の指定にはクオータニオンが使われて、回転軸ベクトルとそのベクトル周りの回転角度を指定することで任意の回転の指定が可能になっている。このプログラムでは回転軸のベクトルを(0,0,1)にしてZ軸座標プラス方向にしているので、プラスの回転角度を入力した時、Z軸プラスの側から見ると、時計回転になる。だからティーポットの回転角度は(180−i)度回転すればいい事になる。

fig04

「Quat」クラスのコンストラクタは可変引数になっていて、help()を使って「__init__」を調べてみたら

print help(MaxPlus.Quat)

以下のようになっていた。

__init__(self, *args)
__init__(Autodesk::Max::Quat self) -> Quat
__init__(Autodesk::Max::Quat self, float X, float Y, float Z, float W) -> Quat
__init__(Autodesk::Max::Quat self, double X, double Y, double Z, double W) -> Quat
__init__(Autodesk::Max::Quat self, Quat a) -> Quat
__init__(Autodesk::Max::Quat self, AngAxis aa) -> Quat
__init__(Autodesk::Max::Quat self, Matrix3 m) -> Quat
__init__(Autodesk::Max::Quat self, Point3 V, float W) -> Quat

一見すると引数に(x,y,z,w)をとるコンストラクタに回転軸ベクトル(x,y,z)と回転角度wを入れたらよさそうなんだけど、回転ベクトル(vx,vy,vz)の長さは1にしなくちゃならないし、回転角度をθとした時にこんな形で入力する必要がある。

Quat(vx*sin(θ/2), vy*sin(θ/2), vz*sin(θ/2), cos(θ/2))

もちろんこの形でクォータニオン作ってもいいんだけど、なんか読みにくいのでこのプログラムでは「AngAxis」の形で回転軸と回転角度をあらわす値を作ってからそれを引数にしてクォータニオンを生成する形をとっている。

angle_rotate = 180-i
angle_axis_rotation = MaxPlus.AngAxis(0,0,1, math.radians(angle_rotate))
quat = MaxPlus.Quat(angle_axis_rotation)
node.SetLocalRotation( quat )

「AngAxis」を使わない場合はこんな感じになる。

angle_rotate = 180-i
th = math.radians(angle_rotate)) / 2
quat = MaxPlus.Quat(0,0,math.sin(th),math.cos(th))
node.SetLocalRotation( quat )

それではまた次回。

maxまとめページ



take_z_ultima at 11:30|この記事のURLComments(0)TrackBack(0)3ds Max | CG

2014年02月25日

ACSを使ってみた その55 modo701 SP2

goto fail;
goto fail;

コーヒー吹いたw

前回作った選択アイテムのポーズを登録するスクリプトについてコードの主要部分について書いておくよ。このスクリプトはACSのリグ用になっているけどちょっといじれば他の目的でも利用可能だ。

このプログラムの大まかな動作は以下の通り。

  1. 選択したアイテムのポーズを外部ファイルに書き出す
  2. 新規にアクションを作成する
  3. 書き出したポーズを読み込む
  4. ポーズの登録
  5. アクションの削除
  6. 選択状態の復帰

これで選択したアイテムのみのポーズがポーズ登録されるわけだ(本来はこんなことしなくても出来るはずなんだけどね)。

まずメインルーチンから。

「tmpActionName」は新規に作るアクションの名前を記録しておく変数。ここに「tmpAction12345」という名前を設定しているけど不都合がある場合は適当に変えればいい。同様に「BufferFileName」はポーズを外部に書き出す時のファイル名の変数。今回は作業フォルダーのパスをクエリして、そのフォルダーに「PoseCopyBuffer.txt」という名前で書き出している。これも都合が悪ければ適当な名前に変えればいいね。

tmpActionName = 'tmpAction12345'
TempPath = lx.eval('query platformservice path.path ? temp')
BufferFileName = TempPath+'\\PoseCopyBuffer.txt'

次に現在選択中のロケータのIDを全て変数「selItems」に取得した。

selItems=lx.evalN('query sceneservice selection ? locator')

次に角度の扱いを全て「度」で行う設定にした。こうしておかないと「ラジアン」と「度」がごっちゃになるからね。

lx.setOption( "queryAnglesAs", "degrees" )

ここから先は「try〜exception」でくくってエラーダイアログとかの処理をしてある。「panel」は入力ダイアログを出して登録するポーズの名前を取得する。結果として変数「poseName」にポーズ名が入る。「copyPose」は選択したアイテムのポーズをファイルに書き出す。「lx.eval('layer.active ? type:actr')」は現在選択されているアクションを調べている。これはあとでアクションの選択をこれに戻すための措置だ。「createTmpAction」は新規のアクションを作成。「pasteSelPose」はファイルに書き出したポーズを読み込む。この時点ですでにアクションは新規のものになっているので、まっさらな状態のリグにポーズがペーストされる。「createPose」はポーズを登録する。「deleteTmpAction」は新規に作って作業したアクションの削除。「lx.eval('layer.active %s type:actr' % activeLayer)」は先に調べておいたアクションを現在のアクションに設定しなおす。そして最初に記録しておいた選択アイテムを再び選択状態に戻してメインルーチンは終了だ。

try:
 poseName = panel('ACSSelectionPoseName',None,'string','New Pose Name:',None)
 copySelPose()
 activeLayer = lx.eval('layer.active ? type:actr')
 createTmpAction()
 pasteSelPose()
 createPose(poseName)
 deleteTmpAction()
 lx.eval('layer.active %s type:actr' % activeLayer)

 #reselect selected items
 if len(selItems) != 0:
  lx.eval('select.item mode:set item:%s' % selItems[0])
  for item in selItems:
   lx.eval('select.item mode:add item:%s' % item)

except ValueError,message:
 lx.eval('dialog.setup error')
 lx.eval('dialog.title "Error"')
 lx.eval('dialog.msg "%s"' % message)
 lx.eval('dialog.open')
except SystemExit:
 dummy=0

次にここで呼ばれているファンクションについての説明だ。「createPose」はポーズを登録するファンクション。単にコマンドを1つ発行しているだけだけどね。「’ ’」で囲まれた文字列中の「%s」で書かれた部分がその後ろに「%」を挟んで書かれた変数の内容で置き換わってmodoにコマンド文字列として渡される。「poseName」には入力されたポーズ名が入っているはずだ。

def createPose(poseName):
 lx.eval('group.poseCreate name:"%s" srcItems:actor srcChans:edits transOnly:false' % poseName)

残りのパラメータは「ソース:アクターアイテム、ソースチャンネル:編集、トランスフォームのみ:OFF」に設定している。

fig01

特に重要なのは「ソースチャンネル」を「編集」にしている事で、「pasteSelPose」でポーズを読み込んだチャンネルのみ「編集」状態になるので、こうしておけばアクターのポーズの中でペーストされた部分だけが新規ポーズに記録されるわけだ。

「createTmpAction」は新規にアクションを作成する。これもコマンドを1つ発行してるだけだ。

def createTmpAction():
 lx.eval('group.layer name:%s grpType:actr' % tmpActionName)

「deleteTmpAction」はアクションを削除するファンクション。実は新規に作成したアクションのアイテムIDの取得方法がわからなかったので、全アイテムの名前を端から調べて「tmpActionName」に設定されている名前と一致するものを見つけ出すことでIDを調べている。アイテムIDが見つかったらあとはそのアイテムを選択して削除しているだけだ。

def deleteTmpAction():
 n = lx.eval('query sceneservice item.N ? all')
 for i in range(n):
  name = lx.eval('query sceneservice item.name ? %s' % i)
  if name==tmpActionName:
   id = lx.eval('query sceneservice item.id ? %s' % i)
   lx.eval('select.item %s set' % id)
   lx.eval('item.delete actionclip')
   break

続きはまた次回。

modo701ブログ目次



take_z_ultima at 11:30|この記事のURLComments(0)TrackBack(0)modo | CG

2014年02月24日

2014の新機能を調べてみた その84 3dsmax 2014

前回に引き続き3dsMAXの拡張機能のPythonスクリプトについて調べてみたい。

前回マテリアルが出てきたので今回は「DemoMaterials.py」を調べてみたい。

fig07

このスクリプトのメインルーチンは「DoStuff()」の1行のみで、すぐ上の「DoStuff()」ファンクションを呼び出しているだけだ。

def DoStuff():
 MaxPlus.FileManager.Reset(False)
 # maximize the view
 MaxPlus.ViewportManager.SetViewportMax(True)
 CreatePlane()
 materials = CreateMaterials()
 CreateAndAssignMaterials(materials)

DoStuff()

「DoStuff()」の中では「FileManager」と「ViewportManager」というなかなか使えそうなクラスが登場している。MaxPlusを調べてみたら「〜Manager」という名前のものが13個見つかった。名前からして用途が想像できそうな感じだ。

  • ActionManager
  • AssetManager
  • BitmapManager
  • ContainerManager
  • FileManager
  • LayerManager
  • MaterialManager
  • MenuManager
  • NotificationManager
  • PathManager
  • PluginManager
  • SelectionManager
  • ViewportManager

今回登場する「FileManager」はマニュアルに

Static functions for working with files. For example loading, saving, importing, exporting, merging and so forth.

と書いてあって、Maxのファイル操作ファンクションを集めたもののようだ。

fig01

公開されているファンクションは以下の通り。MAXのファイルメニュに現れる機能の操作はほぼこれで出来そうだ。

fig03

最初に出てくる

MaxPlus.FileManager.Reset(False)

の部分はMax自体をリセットするための記述だ。このスクリプトではコンパクトマテリアルエディタのスロットを使ったりするのでその前に全部初期化しちゃおうって事らしい。

fig06

引数の「False」はリセットしていいかどうかの警告ダイアログを出さないようにするための指定だ。

fig02

次の「ViewportManager」はもちろんビューポートの操作関係のファンクションが集められたものだ。このスクリプトに出てくる

MaxPlus.ViewportManager.SetViewportMax(True)

の部分はアクティブビューポートを最大化するかどうかの切り替えで、引数がTrueなら最大化、Falseなら元に戻す。

fig05

ここまででMaxがリセットされてアクティブビューポート(パースビュー)が最大化される。

次の

CreatePlane()

の部分は120X120のサイズの「Plane(平面)」オブジェクトを作ってノードにセットしてシーンに表示する。以前に出てきた「ParameterBlock」を使って生成した平面オブジェクトに幅と長さのパラメータを設定している。パラメータブロックは生成されるオブジェクトにあわせてプロパティ名が変化する便利なクラスだったね。これのお陰で球もBOXもみんなGeomObjectのまま、PlaneならWidthやHeight、SphereならRadiusという名前でパラメータにアクセス出来ちゃうわけだ。

def CreatePlane():
 plane = MaxPlus.Factory.CreateGeomObject(MaxPlus.ClassIds.Plane)
 plane.ParameterBlock.Width.Value = 120.0
 plane.ParameterBlock.Length.Value = 120.0
 node = MaxPlus.Factory.CreateNode(plane)

fig04

ちなみにこういう仕組みはおそらくPythonのクラスに「特殊メソッド名」を持ったメソッドを定義することで実現してるんじゃないかな。

例えばオブジェクト「obj」の「ParameterBlock」の「radius」という名前のパラメータに値を代入する時、

obj.ParameterBlock.radius.Value = 10

なんて書くわけだ。でもパラメータは同じGeomObjectであっても球とBOXなど、種類によってまるで違うわけで、「ParameterBlock」クラスに全てのパラメータへアクセスするための属性を定義して置く事は不可能だし、出来たとしても将来の拡張には備えようも無い。だから本来、同じ「ParameterBlock」クラスのオブジェクトに対して異なる属性名を指定してアクセスするなんて出来ないはずだ。

この「A.B」って形で書がれたものは、Aの中にあるBという名前のメンバーを呼び出すわけだけど、PythonではBが見つからない場合、値の代入なら「__setattr__」、値の参照なら「__getattr__」という特別の名前のメソッドを呼び出す仕組みが提供されている。

そして、「A.B」なら「B」の方が文字列としてこのメソッドに引数で渡される。だから、

obj.ParameterBlock.radius

となっているなら

obj.ParameterBlock.__getter__(self.'radius')

と解釈されて実行される。ここで「ParameterBlock」クラス内部で「__getter__」が定義されていて、そこで「'radius'」を引数に「GetParameterByName」が呼び出される仕組みになっていれば、

class ParameterBlock:
 def __getter__(self,arg):
  return GetParameterByName(self,arg)

「radius」という名前でパラメータオブジェクトを見つけて来てくれて、そのオブジェクトの「Value」プロパティに10が代入されて、球体の半径が10になるわけだ。

このような仕組みがあるから同じ「ParameterBlock」クラスなのに見かけ上、バラバラな名前のプロパティを持つ事が許されちゃうんだね。たぶん・・・。

さて、次の行の

materials = CreateMaterials()

は「CreateMaterial」がマテリアルオブジェクトをリストの形で生成して変数「materials」に受け取る。呼び出されるファンクションは以下の通りだ。

def CreateMaterials():
 materials = GeneratePlugins(MaxPlus.SuperClassIds.Material, MaxPlus.Mtl)
 materialList = list(materials)
 return materialList

これには前回も出てきたジェネレータ「GeneratePlugins」が使われている。「GeneratePlugins」は指定したスーパークラスを持つクラスのオブジェクトを1つずつ順に全て生成していくもので、シーケンスデータのように振舞って、シーケンスの要素が必要になった時に1つずつ生成して返す仕組みになっていた。

今回は引数としてスーパークラスIDが「Material」、キャストするクラスが「Mtl」として設定している。これで全ての種類のマテリアルオブジェクトを持つシーケンスデータのように振舞うファンクションになる。

def GeneratePlugins(sid, cls):
 for cd in MaxPlus.PluginManager.GetClassList().Classes:
  if cd.SuperClassId == sid:
   anim = MaxPlus.Factory.CreateAnimatable(sid, cd.ClassId, False)
   if anim:
    inst = cls._CastFrom(anim)
    if inst:
     yield inst

プログラムではこのジェネレータをまず変数「materials」に受け取っている。この時点では「GeneratePlugins」は実行されず、変数に格納されるだけだ。次に変数「materials」が組み込みファンクションの「list」に渡されて、はじめてジェネレータが順に呼び出されて、新規に作成されたリストオブジェクトに次々登録される。

要するにこのプログラムではせっかくのジェネレータを「list」で展開しちゃって全部生成して貯めちゃってるわけだ。なんでそんなことしちゃってるかと言ったらマテリアルの個数が欲しかったのが原因みたいだ。リスト内の要素数は組み込みファンクションの「len()」を使って調べる事が出来るけど、ジェネレータでは受け付けてくれないみたいだ。まあ作ってみないとわからないからジェネレータを使うわけで、作らないうちにいくつ出来るか聞くのは酷ってもんだね。

こうして得られたリスト化したマテリアルオブジェクトを使って

CreateAndAssignMaterials(materials)

でマテリアルエディタのスロットにマテリアルを割り当てたり、マテリアルを割り当てたティーポットを並べたりするわけだけど、それはまた次回。

maxまとめページ



take_z_ultima at 11:30|この記事のURLComments(0)TrackBack(0)3ds Max | CG
Archives