2011年08月

2011年08月31日

max script 勉強してみた その11 3dsmax 2012

前回はブロック式を使った時に起きる不思議な現象を紹介したところで終わった。今回はその続き。

もう一度書いておくと、MaxScriptリスナーでブロック式を含む代入式を書いてみると代入したはずの変数efgには値が入っておらずundefinedになるけど、

abcd=(efg=20)
20
abcd
20
efg
undefined

同じ式をもう一度実行してみると今度はefgに値が代入されるというものだった。

abcd=(efg=20)
20
abcd
20
efg
20

タネ明かしをすると、これは変数の有効範囲(スコープと言う)が原因だ。MaxScriptでは変数は最初に出てきた場所で勝手にメモリーに割り当てられる。そしてそれがどこで行われるかによって、変数の有効範囲が決定する。それがもしブロック式内であれば、その変数の有効範囲は一番外側のブロック式内だけで、そこから出てしまえば変数は無効になる。上の例で

(efg=20)

の部分はまさにそれで、efgは20を代入されるまでは有効だけど、このブロック式の評価が終わった時点で無効になる。だから式の評価が終わった後でefgを評価してみても20が出て来ないわけだ。

じゃあ何で2度目には20って出てきたのかと言うと、1度目の式評価の後でefgと入力して変数を評価した時点で変数efgが作成されていたためだ。この変数の有効範囲はabcdと一緒で、式の評価が終了しても無効にならない。そこで改めて式

abcd=(efg=20)

を評価すると、すでに変数abcdもefgも存在するので改めて変数が生成されることはなく、有効範囲がブロック式より外側の変数efgに20が代入されるため、式の評価が終了しても変数が有効で、代入された値20が保持されていたわけだ。

このように有効範囲が処理全体に及ぶものをグローバルスコープ、局所的なものをローカルスコープと言う。グローバルなものは調査関数aproposやそのエイリアスのhelpで調べられる。

apropos "abcd"
abcd (Integer): 20
OK

またグローバル変数をglobalvars.remove( )メソッドで削除することも出来る。

globalvars.remove "abcd"
true
apropos "abcd"
OK

MaxScriptは変数が実行しているコード上に現れた時、その時点でその変数が存在しなければ自動的に変数を生成する。これを「暗黙的な変数宣言」と言う。そして変数が生成された時点でその変数のスコープも決定するわけだけど、その範囲がどうなるかはその時点で開かれているスコープコンテキストで決まる。スコープコンテキストの初期値はグローバルで、新しいスコープコンテキストが開かれるタイミングはマニュアルに以下のようにまとめられている。

  • スクリプト ファイルまたはリスナーにおけるトップ レベルの始まりのカッコ「( 」
  • 関数本体の始まり
  • for ループ本体の始まり
  • ユーティリティ、ロールアウト、右クリック メニュー、マクロ スクリプト、またはツール定義の始まり
  • ユーティリティ、ロールアウト、右クリック メニュー、マクロ スクリプト、またはツール イベント ハンドラの始まり
  • when ループ本体の始まり

ここでよーく注意しなくちゃならないのは、ブロック式の中にブロック式を作ってもそれはトップレベルのカッコにならないので新しいスコープコンテキストは開かれないってことだ。

まず同じ名前のグローバル変数があったらまずいので削除作業をしておく。

globalvars.remove "a"
false

続いてブロック式でa変数に10を代入してそのブロック内でformat関数を使ってaの値を出力させてみた(ちなみに”\n”は”¥n”で改行を表わす文字ね)。その後で変数aを評価してみるとundefinedになった。

(a=10 ; format "a = %\n" a )
a = 10
OK

a
undefined

これは想定通りなわけだ。

次に今度はブロック式を2重にして一番内側の変数aに10を代入してみた。まずは前回変数aを評価した時点でグローバルスコープに出来上がっちゃうので改めて削除してから式を入力して評価してみた。

globalvars.remove "a"
true
( ( a = 10) ; format "a = %\n" a )
a = 10
OK
a
undefined

見ての通りaが最初に登場する2重カッコの内側でaに10を代入したら、そのカッコの外で評価した変数aの値が10になっている。そしてグローバル変数としてのaはundefinedだ。これはカッコが何重になってもその中のローカル変数は同じものだってことを表わしている。これがスコープコンテキストが開かれるタイミングが

スクリプト ファイルまたはリスナーにおけるトップ レベルの始まりのカッコ「( 」

と言う意味だ。あくまで一番外側のカッコだけがスコープの範囲と関係しているわけだね。

ちなみにグローバル変数aが存在する場合、aはすでにスコープの外側に存在するので新たなローカル変数は作成されず、そのままグローバル変数aが使われるので、式終了後の変数を評価すると10が出てくる。

( ( a = 10) ;format "a = %\n" a )
a = 10
OK

a
10

もしグローバル変数aが存在する時に新しく開かれたスコープコンテキストのローカル変数aが必要なら、明示的にローカル変数を宣言することも出来る。これを「明示的な変数宣言」と言って、次の書式でグローバル変数もローカル変数も宣言できる。

( local | global ) <decl> { , <decl> }

  <decl> ::= <var_name> [ = <expr> ]

この書式によれば、グローバル変数もローカル変数もカンマで区切って一度にいくつも宣言できるし、その中に初期化のための代入式を書いてもいい。

local a, b, c = 10, d = 20 ,e

みたいな感じだ。さてそれを取り入れてカッコ内でローカル変数を宣言してみる。グローバル変数aはそのまま削除していないので10が入っている。

( local a = 20 ; format "a = %\n" a )
a = 20
OK

a
10

となって、確かにブロック式内ではローカル変数aが使われて、ブロック式の外ではグローバル変数aが使われているのがわかる。

逆にグローバル変数としてローカルスコープ内で変数を宣言することもできる。

globalvars.remove "a"
true
( global a = 20 ; format "a = %\n" a )
a = 20
OK

a
20

さらに、ローカル変数でグローバル変数が覆い隠されている時に「::」演算子を使うとグローバル変数にアクセスすることもできる。

a=100
100
( local a = 20 ; format "a = %\n" a )
a = 20
OK

( local a = 20 ; format "a = %\n" ::a )
a = 100
OK

2番目のブロック式のformatのところで「::a」とすることで20に初期化されたローカル変数aではなくてグローバル変数aがアクセスされて値100が出力されている。

スコープコンテキストが新たに開かれるのは何もブロック式だけじゃないのは、この記事の前半で触れたけど、例えば「for ループ本体の始まり」もその一つだ。そしてfor文では特にループカウンタで使用される変数は必ずfor文のスコープコンテキストのローカル変数になる。例えば

for i = 0 to 5 do print i
0
1
2
3
4
5
OK

i
undefined

とした時、変数 i はローカル変数になる。だからfor文を終了した後で変数iを評価するとundefinedになる。しかもこれはグローバル変数の変数iがあっても強制される。

global i = 100
100
for i = 0 to 5 do print i
0
1
2
3
4
5
OK

i
100

for文が開始されるとそこからは新しいスコープコンテキストが開かれるので、先にやったブロック式の中にfor文を書けば、for文の中のローカル変数は外側のブロックのローカル変数とは別のものになる。太字にしたのがfor文で、doの後ろの式がブロック式になっている。このブロック式はfor文の一部なので当然for文の開始位置で新しく開かれたスコープコンテキストになる。だから内側で変数aがローカル宣言されると外側のブロック式で宣言された変数aとは異なるローカル変数になり、初期化されないので初期値としてundefinedが設定される。そこで次のif文でaの値がundefinedであればaに0を代入するという役割のif文をいれ、変数aを0で初期化している。次にa=a+1で現在の変数aの値に1を加えたものを変数aに代入することでaの値を1増加させ、最後にformat文で出力させている。

global a=100
100
(
 local a = 20
 for i = 0 to 5 do (
  local a
  if a == undefined then a = 0
  a = a + 1
  format "local 2 : %\n" a
 )

 format "local 1 : %\n" a
)
local 2: 1
local 2: 1
local 2: 1
local 2: 1
local 2: 1
local 2: 1
local 1 : 20
OK

この結果からわかるのは、for文のスコープコンテキストはループ変数を除くとdoの後ろの式の部分で開始されるってことだ。だからその式がfor文で繰り替えされるたびにローカル変数aが宣言されて、0で初期化されて1が加えられて出力されるから「local 2: 1」が6回繰り返し出力されている。そしてそこを抜けると外側のスコープコンテキストに戻ってfor文より前に宣言されたローカル変数aの値20が出力されている。

もしfor文の中で変数aをローカル変数宣言しなkれば、外側のスコープのaが使われて初期値20でそれに1加えられた21から出力が始まる。当然ループを抜けてもローカル変数の値は維持されて26が出力される。そしてブロックの外ではグローバル変数になるので値は100だ。

global a=100
100
(
 local a = 20
 for i = 0 to 5 do (
  if a == undefined then a = 0
  a = a + 1
  format "local 2 : %\n" a
 )

 format "local 1 : %\n" a
)
local 2 : 21
local 2 : 22
local 2 : 23
local 2 : 24
local 2 : 25
local 2 : 26
local 1 : 26
OK

a
100

いきなり制御文まで出てきたからややこしいけど、要するに前半で列挙したスコープコンテキストが開始される位置によって変数の有効範囲は切り分けられるって事だ。こういうのはマニュアルをちゃんと読まないで適当にやってると痛い目を見やすいな。

それではまた次回。

maxまとめページ



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

2011年08月30日

modo501 SDK いじってみた その36 modo501 SP3

今回はvertvalue.cppのbasic_ArgType ( )を調べて見たい。

まずこのメソッドの注釈をみるとこう書いてある。

This method will be called to get the type of the variable argument type, which in our case is the 'type' argument. 'index' will be ARGi_VALUE, so we don't really have to test that. We pick a datatype appropriate for the map type.

このメソッドは、可変引数タイプ、この場合は「type」引数、の型を得るために呼び出される。「index」はARGi_VALUEになるので、私たちはそれを本当にテストする必要はない。我々はそのマップタイプに対して適切にデータタイプを選ぶ。

訳はあってるかどうかわからない・・・。でも要するに型が変化する引数の型を返すメソッドのようだね。そして今回のコマンドの場合、3つの引数があるけど、

vertex.componentValue
  component:integer ?value:* <type:string>

型が変化するのは2つ目のvalueだけだから、indexはこの引数しか指さないからテストの必要は無いと解釈できる。だからこのメソッドをオーバーライドして実装する必要があるのは、型が変化する引数がある場合のみのようだ。

以下がそのコード。確かに引数のindexは使われていない。大雑把に見て行くと、GetVMap( )で現在選択しているVMapの種類を調べてcm_typeに反映し、それをもとに戻り値を決めているんじゃないかと推測できる。

 const char *
CVertexCompCommand::basic_ArgType (
 unsigned int index)
{
 if (!GetVMap ())
  return LXsTYPE_FLOAT;

 if (cm_type == LXi_VMAP_TEXTUREUV)
  return LXsTYPE_UVCOORD;

 if (cm_type == LXi_VMAP_WEIGHT)
  return LXsTYPE_PERCENT;

 if (cm_type == LXi_VMAP_MORPH
  || cm_type == LXi_VMAP_SPOT)
  return LXsTYPE_DISTANCE;

 if (cm_type == LXi_VMAP_RGBA
  || cm_type == LXi_VMAP_RGB)
  return LXsTYPE_COLOR1;

 return LXsTYPE_FLOAT;
}

そこでコードを詳細に見て行くと、まずGetVMap ( )は以下のようになっている。

 bool
CVertexCompCommand::GetVMap ()
{
 void *pkt;

 /*
 * If the 'type' argument is unset,
 * we just take the most recent vmap
 * selection and read it's name and type.
 */
 if (!dyna_IsSet (ARGi_TYPE)) {
  pkt = srv_sel.Recent (selID_vmap);
  if (!pkt)
   return false;

  pkt_vmap.Name (pkt, &cm_name);
  pkt_vmap.Type (pkt, &cm_type);
  return true;
 }

 /*
 * If the type is set, we decode it and
 * then walk the selection to find
 * a map that fits the request.
 */
 string typeStr;
 LXtID4 type;
 unsigned i, n;

 attr_GetString (ARGi_TYPE, typeStr);
 if (LXx_FAIL (srv_mesh.VMapLookupType (
            typeStr.c_str (), &type)))
  return false;

 n = srv_sel.Count (selID_vmap);
 for (i = 0; i < n; i++) {
  pkt = srv_sel.ByIndex (selID_vmap, i);

  pkt_vmap.Type (pkt, &cm_type);
  if (cm_type != type)
   continue;

  pkt_vmap.Name (pkt, &cm_name);
  return true;
 }

 return false;
}

まず最初にやっているのはvertex.componentValueコマンドの3つ目の引数である<type:string>について、これは省略可能な引数なのでdyna_IsSet ( )メソッドでセットされているかを調べている。ここで出てくるARGi_TYPEや先に出てきたARGi_VALUEはインデックスによって引数を指し示すものらしく、このように定義されている。

#define ARGi_COMP 0
#define ARGi_VALUE 1
#define ARGi_TYPE 2

CVertexCompCommandクラスのコンストラクタCVertexCompCommand ( )の中で、次のように引数が追加されているので、この順番にあわせた通し番号なわけだ。

CVertexCompCommand::CVertexCompCommand ()
{

           :

 dyna_Add ("component", LXsTYPE_INTEGER);
 dyna_Add ("value", LXsTYPE_FLOAT);
 dyna_Add ("type", LXsTYPE_STRING);

           :
}

引数<type:string>が設定されていない場合、if文のブロックが実行されることになる。その時何が行われるかは注釈に書いてあって、

If the 'type' argument is unset, we just take the most recent vmap selection and read it's name and type.

もし「type」引数がセットされていなければ、最新のVMap選択を採用して名前と型を読む

とある。if文のブロックのところで最初に

pkt = srv_sel.Recent (selID_vmap);

をして最新のVMap選択を取得してるみたいだな。
srv_selはCLxUser_SelectionService型で、CLxUser_SelectionServiceはグローバルサービスのILxSelectionServiceのユーザーラッパークラスだ。

これを使ってCVertexCompCommandクラスのコンストラクタCVertexCompCommand ( )の中で、

selID_vmap = srv_sel.LookupType ("vmap");

とやって「vmap」という名前から型のIDのようなものを得ているようだ。これをキーにしてsrv_sel.Recent (selID_vmap)でvmapの最新の選択が取得できるんだろうね。

得られた値はパケットという単位になるらしい。ドキュメントのlx_select.htmの冒頭のところにこう書いてある。

Individual selections themselves are defined as opaque packets which are allocated and managed by the selection system. For extracting specific information about the selection, the selection type can provide an alternate packet translation interface.

個々の選択自体は、選択のシステムによって割り当ておよび管理されている不透明なパケットとして定義されています。選択についての具体的な情報を抽出するため、選択の種類は、代替のパケットの翻訳インターフェースを提供することができます。

で、そのパケットの翻訳インターフェースで名前やタイプを抽出してるのがこの部分だろうね。

  pkt_vmap.Name (pkt, &cm_name);
  pkt_vmap.Type (pkt, &cm_type);

pkt_vmapはCLxUser_VMapPacketTranslationタイプで、これもユーザーラッパークラスだ。 ILxVMapPacketTranslationインターフェースについてはlx_seltypes.htmに以下の記述がある。

A selected vertex map consists entirely of a name and a type. Commands affect all selected mesh items that contain a vertex map with that name and type.

選択した頂点マップは、名前と型で完全に構成されています。コマンドはその名前とタイプの頂点マップを含むすべての選択したメッシュアイテムに影響を与えます。

いろいろ出てきてややこしくなってきたので続きはまた次回。

カテゴリー別ページ



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

2011年08月29日

max script 勉強してみた その10 3dsmax 2012

そろそろちゃんとプログラムを見て行きたいところなんだけど、その前に文法をちょっとおさえて置きたい。

まず名前について。ちょっと変数のところまで広げてマニュアルを見ると

  1. 名前は変数、関数、パラメータ、プロパティなどの識別に使い、アルファベットまたは「_ 」(アンダースコア)で始まり、任意の英数文字または「_ 」を含む事ができる。
  2. 大文字と小文字は区別されない。
  3. #<var_name> とすると名前を数や文字列のように値として扱うことができ、また配列の添字としても使える。
  4. '<any_char_except_quote>' として文字列を単一引用符で囲むと、スペースやその他の区切り記号など、名前として不正な識別子になる文字を名前として使う事が出来る。
  5. オブジェクト名などでスペースを含む名前をmaxScriptで扱う時にスペースを「_ 」で置き換える事ができる。
  6. 予約済みのキーワード、予約済みグローバル変数があり、グローバル変数はグローバルスコープから常にアクセスできる。

とある。

1、2については他のプログラム言語でもよくあるものなのでいいだろう。

3は「#名前」とすることでそれを定数として使う仕組みだ。C言語などの列挙型みたいなもんで、maxscriptではそれを定義もしないでいきなり使える。例えば

animal=#cat
if animal == #dog then print "bow" else print "mew"
"mew"
"mew"

のように、いきなり#dogとか#catという定数があるものとして使っちゃって構わない。これはプログラムを読みやすくするのが目的だ。だから#dogが別に犬として何か機能しなきゃならないって事はないし、演算でできるのは代入と交換と比較くらいなもんだ。また配列の添字としても使えて、例えば前にちょっと出てきたモディファイヤ配列で、オブジェクトBOX001にベンドモディファイヤが設定されているとき、そのモディファイヤにアクセスするのに、

$box001.modifiers[#Bend]
Bend:Bend

などとやれば、配列の中から最初に見つかったベンドモディファイヤを参照できる。

4と5も前にちょっと出てきたけど、例えばシーン内のオブジェクトの名前に「Box Red」みたいにスペースを含む名前がある時、スペースはプログラムの中では区切り文字なのでそのまま使うとスペースの位置で名前が区切られてしまってエラーになる。

$Box Red
-- タイプ エラー: 呼び出しには関数かクラスが必要です, 取得したのは: undefined

しかし単一引用符で囲むかスペースをアンダースコアで置き換えればそれを回避できるって話だ。

$'Box Red'
$Editable_Mesh:Box Red @ [1.826234,0.019445,0.000000]

$Box_Red
$Editable_Mesh:Box Red @ [1.826234,0.019445,0.000000]

ちなみにスペース以外で名前に使えない文字が入ってる時は単一引用符で囲む方法しかダメだね。そしてそれが「¥」だとエスケープシーケンスが働いちゃうのでその時は「¥¥」と2つ重ねる必要がある。まあそんな文字をオブジェクトの名前につけないのがいちばんだけどね。

話ついでに書いておくと、ファイルパスなんかも「¥」を使うので、

fpath = "c:¥test.txt"
"c: est.txt"

とすると「¥t」がエスケープシーケンス文字と見なされて¥tはタブ文字を表わすからタブのコードに変換されてしまう。だから¥は¥をあらわすエスケープシーケンス¥¥を使って

fpath = "c:¥¥test.txt"
"c:¥test.txt"

としなくちゃならないわけだけど、max2008から追加された逐語的文字列リテラルを使うとエスケープシーケンスの変換を無視できる。

@"{<character>}+"

これを使ってパスを書くなら

fpath = @"c:¥test.txt"
"c:¥test.txt"

でOKだ。これはあくまで文字列定数なので名前として使えるわけじゃないからね。

6はかなりいっぱいあるからここで全部は触れられないけど、MaxScriptの言語の予約語だけでこれだけある。

about,and,animate,as,at,by,case,catch,
collect,continue,coordsys,do,else,exit,
fn,for,from,function,global,if,in,local,
macroscript,mapped,max,not,of,off,on,or,
parameters,persistent,plugin,rcmenu,
return,rollout,set,struct,then,throw,to,
tool,try,undo,utility,when,where,while,with

これらは他の用途では使う事が出来ない名前だ。例えば変数名として使うとエラーになる。

about = 5
-- シンタックス エラー : = で、<center expr> が要求されます。
-- 次のライン : about=5

定義済みグローバル変数はaproposとかの調査関数のところでやったけど、検索してみると物凄い数があるのがわかる。最低でも覚えておくべきなのはこんなところかな。

true , false , pi , undefined , unsupplied

trueとfalseについては説明はいらないね。piは円周率のπのことだ。undefinedは変数や配列要素の初期値で、まだ初期化されていないことを表わす。オブジェクトを参照している変数にundefinedを代入すればリンク数が減るからリンクが0になればガベッジコレクションの対象にもなるし、無用なメモリーを解放する手助けになるかも。

unsuppliedは関数のキーワードパラメータが初期化されない時の値で、これについては関数と一緒に説明するね。

このほかにもtrue/falseのかわりにon/offとか、色の定数

red, green, blue, white, black, orange, yellow, brown, gray

なんてのもある。リスナーパネルで入力して評価してみると

red
(color 255 0 0)

と出てくる。

3軸の単位ベクトルの定数なんてのもある。

x_axis, y_axis, z_axis

評価してみると

x_axis
[1,0,0]

となる。こういうのをちょっと知ってると読みやすいコードが書けるんだよね。でも使う頃には忘れてるんだよなぁw

次に変数について。

MaxScriptの変数は宣言しなくてもいきなり使えるし、タイプ自由変数なので任意の値を保持する事ができる。かなり柔軟で便利だけど、こういうのはバグを見つけにくくする元凶でもあるわけだね。

変数に値を代入するときは

<destination> = <expr>

の形式で行う。「=」の左側に代入される変数名、右側に代入する値として評価される式がくる。例えば

a=10+5
15

とすればaには式(10+5)の評価結果15が代入されるわけだね。

また2つの変数の値を入れ替えるswap ( ) というメソッドもある。

a = 10
10
b = 20
20
swap a b
10
a
20
b
10

交換する変数の値の型が違っても問題ない。

a=x_axis
[1,0,0]
b=#cat
#cat
swap a b
[1,0,0]
a
#cat
b
[1,0,0]

これを知っていれば、変数の値を入れ替えるために一時的な変数を用意する必要もないね。

ところで上の例もそうだけど、リスナーでコードを入力して実行するとその下に青い字で何か表示される。「a=10+5」の例でいえば「a=10+5」と入力すると青い字で15と表示される。これは「a=10+5」という式の値だ(10+5の値じゃなくてそれを変数aに代入するところまで含めた式の値ね)。式の値であればそれを計算に使ったり変数に代入したりも当然できるわけで、

b = a = 10 + 5
15

とすれば、式(10+5)が計算されてその値15が変数aに代入され、その代入式の値が15になるのでそれが変数bに代入され、その式の値が15なので青い文字でリスナーに表示されるわけだ。

単純に「式」(expr)って書いてるけどスクリプトの中ではリスナーで入力して評価して値が表示されるものはみんな式だ。だからありとあらゆるものが式だ。単純に数値1個書いてもそれは定数リテラル1つの式であるし、値を比較したり、関数を呼び出したり、いろいろだ。要するにMaxScriptは式によって出来ている言語だと言う事もできる。マニュアルにも

MAXScript は、式ベースの言語です。式には、他の言語がステートメントとして扱う構文も含まれます。これにより、言語の構文が簡略化され、表現力に富んだコードを構築できます。 式を書き込むことができるすべての場所に、MAXScript の任意の構文を書き込むことができます。

と書いてある。この式の中には「ブロック式」と言う式もあって、複数の式を1つの式にまとめる事ができる。文法的にはこんな感じだ。

( <expr> { (; | <eol>) <expr> } )

要するに( )で0個以上の「;」(セミコロン)か改行で区切った式を囲めばそれがブロック式になるってことだ。例えば

d = (
  a = 10 ; b = 20
  c = a + b
)
30

としてやれば、3つの式がひとまとめになる。a=10とb=20はセミコロンで区切って1行におさめて、c=a+bは改行で区切ってある。セミコロンで区切っても改行で区切っても式どうしが区切られる事に変わりは無い。ただどちらが読みやすいかで書き方は選択すればいい。そしてこのブロック式の値は最後に評価された式がその値となる。この場合はc=a+bが最後に評価される式なのでブロック式の値は30になり、それが変数dに代入され、その代入式の評価値がリスナーに青い文字で表示される(今回は30が表示されている)。これでdの値は30だ。

上のブロック式はさほど意味を持たないかも知れないが、文法上で式が書ける部分はこのブロック式も書けるので、単純な式では書き表わせないような複雑な処理も、その処理自体をブロックでまとめてしまえば1つの式として扱えちゃうのがメリットだね。

ところでいきなりだけど、ブロックを使うとこんな事が起こるよ。

abcd=(efg=20)
20
abcd
20
efg
undefined

efgに確かに20を代入したはずなのにundefinedになってる。ところがこれをもう一度やってみると

abcd=(efg=20)
20
abcd
20
efg
20

今度はefgが20になった。

次回はどうしてこんな事が起きるのかって話から始めるよ。

それではまた次回。

maxまとめページ



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

2011年08月26日

modo501 SDK いじってみた その35 modo501 SP3

前回に引き続きCLxBasicCommand::cmd_NotifyAddClient ( )メソッドから。

 LxResult
CLxBasicCommand::cmd_NotifyAddClient (
 int argument,
 ILxUnknownID object )
{

 int i;
 if (pv->initNotifiers) {
  CLxUser_NotifySysService srv;
  CLxUser_Notifier note;
  LXtObjectID obj;
  string name, args;
  LxResult rc;

/* Notifiers that are global to the command */
  i = 0;
  while (basic_Notifier (i++, name, args)) {
   rc = srv.Spawn (name.c_str(), args.c_str(), &obj);
   if (LXx_FAIL (rc))
    throw (rc);

   note.take (obj);
   pv->notifiers.push_back (note);
  }

  if( argument != -1 ) {
/* Argument-specific notifiers */
   LXtObjectID uiValueHintsObj;
   if( LXx_OK( atrui_UIValueHints(
     argument, &uiValueHintsObj ) ) ) {
    CLxLoc_UIValueHints uiValueHints;
    int n;

    if( uiValueHints.take( uiValueHintsObj ) ) {
     if( LXx_OK(
     uiValueHints.NotifierCount( &n ) ) ) {
      for( i=0; i < n; i++ ) {
       const char *name = NULL,
              *args = NULL;
       if( LXx_OK(
       uiValueHints.NotifierByIndex(
       &name, &args ) )
       && (name != NULL) ) {
        rc = srv.Spawn (name, args, &obj);
        if (LXx_FAIL (rc))
         throw (rc);

        note.take (obj);
        pv->notifiers.push_back (note);
       }
      }
     }
    }
   }
  }

  pv->initNotifiers = false;
 }

 for (i = 0; i < pv->notifiers.size (); i++)
  pv->notifiers[i].AddClient (object);

 pv->noteClients.push_back (object);
 return LXe_OK;
}

前回は最初のwhile文のブロックで、オーバーライドして作成したbasic_Notifier ( )の中で定義した(name,args)のセットからNotifierのユーザーラッパーオブジェクトが生成され、それがベクターのnotifiers配列に格納されるというところまで調べた。

この(name,args)のセットはイベントとそれによって更新されるUIをフラグの組み合わせで指定する事が出来る様になっていた。我々はコマンドクラスを定義する時にCLxBasicCommandクラスを継承し、basic_Notifier ( )メソッドを定義するだけで、特別なコードを書かなくてもUIの更新イベントが実装できるようだ。

と言っても見てきたのはまだNotifierを作ってベクター配列に格納するところまでだった。

CLxBasicCommand::cmd_NotifyAddClient ( )メソッドのwhileブロックの次の行のコードを見ると、

 if( argument != -1 ) {
* Argument-specific notifiers */

このif文のブロックは引数argumentに−1が設定されていない場合に実行される。この−1についてはNotifyAddClient ( )メソッドの解説に

If the argument index is -1, then only the notifers defined by the entire command will be added.

もし引数インデックスが-1の場合、全部のコマンドによって定義されているnotifersだけが追加される。

と書いてある。要するにこのifのブロック内での処理は、引数個別のイベント処理についてのnotifierを生成してベクター配列に登録する事のようだ。argumentパラメータが−1じゃない場合は次のコードが実行される。

LXtObjectID uiValueHintsObj;
if( LXx_OK( atrui_UIValueHints(
 argument, &uiValueHintsObj ) ) ) {

このatrui_UIValueHints ( )メソッドの定義は以下で、

 LxResult
CLxDynamicAttributes::atrui_UIValueHints (
 unsigned int index,
 void **ppvObj)
{
 static CLxPolymorph<CUIValueHintWrapper>
      *valFactory = 0;

 CUIValueHintWrapper *wrap;
 CLxDynamicUIValue *val;

 val = atrui_UIValue (index);
 if (!val)
  return LXe_NOTIMPL;

 if (!valFactory) {
  valFactory = new
    CLxPolymorph<CUIValueHintWrapper>;
  valFactory->AddInterface (new
    CLxIfc_UIValueHints<CUIValueHintWrapper>);
 }

 wrap = valFactory->Alloc (ppvObj);
 wrap->val = val;
 return LXe_OK;
}

ファクトリーオブジェクトを使って引数indexで指定した属性に対してUIValueHintオブジェクトを生成して引数ppvObjに返している。ファクトリーを格納する変数valFactoryはstatic宣言されているので0で初期化されるのはこの変数の領域が静的に確保される時の1回だけで、atrui_UIValue (index)がfalseを返さないでこの変数が0の時だけファクトリーオブジェクトが生成されて割り当てられる(要するにファクトリーオブジェクトは生成されたとしても1つだけだ)。

属性についてはatrui_UIValue (index)でindexで指定する属性に対してのLxDynamicUIValueオブジェクトを得ている。

そこでatrui_UIValue (index)を辿ってみると、今回のコードでは

class CLxDynamicAttributes :
 public CLxImpl_Attributes,
 public CLxImpl_AttributesUI
{
 public:

         :

   virtual CLxDynamicUIValue *
  atrui_UIValue (unsigned int index) { return 0; }

         :

};

となっているだけで、indexの値にかかわらず0を返すので結局このatrui_UIValueHints ( )メソッドはLXe_NOTIMPLを返すだけのようだ。他の参照も検索してみると、cmd_vertvalueプロジェクトのもう一つの例である「instsrc.cpp」の方ではatrui_UIValue ()メソッドをオーバーライドして使っているようだ。下はそのコード。

class CItemPopup : public CLxDynamicUIValue
{
 public:
  unsigned Flags () LXx_OVERRIDE
  {
   return LXfVALHINT_ITEMS |
        LXfVALHINT_ITEMS_NONE;
  }

  bool ItemTest (CLxUser_Item &item)
       LXx_OVERRIDE
  {
   return sT->AllowType (item.Type ());
  }
};

 CLxDynamicUIValue *
CInstSourceCommand::atrui_UIValue (
 unsigned int index)
{
 return new CItemPopup;
}

この部分についてはinstsrc.cppを調べるときに詳しく触れたい。

結局cmd_NotifyAddClient ( )メソッドのif( argument != -1 ) { からのブロックのくくりはatrui_UIValueHints( argument, &uiValueHintsObj ) がLXe_NOTIMPLを返すことで全部スキップされる。

もしatrui_UIValueHints ( )メソッドがLXe_OKを返していた場合はuiValueHintsObjにUIValueHintオブジェクトが渡され、それをUIValueHintのローカライズクラスのCLxLoc_UIValueHintsのインスタンスにtake( )メソッドで引き渡され、そこに定義されているnotifierのぶんだけnotifierが生成されてnotifiers配列に格納されるようになっている。下がそのコード部分。

         :

CLxLoc_UIValueHints uiValueHints;
int n;

if( uiValueHints.take( uiValueHintsObj ) ) {
 if( LXx_OK( uiValueHints.NotifierCount( &n ) ) ) {
  for( i=0; i < n; i++ ) {
   const char *name = NULL, *args = NULL;
   if( LXx_OK( uiValueHints.NotifierByIndex( &name, &args ) ) && (name != NULL) ) {
    rc = srv.Spawn (name, args, &obj);
    if (LXx_FAIL (rc))
     throw (rc);

    note.take (obj);
    pv->notifiers.push_back (note);
   }
  }
 }
}

         :

これでコマンドオブジェクトの属性全体に対するnotifierと個別のnotifierをnotifiersベクター配列に格納したわけだ。

次に

pv->initNotifiers = false;

としてこのif (pv->initNotifiers) {・・・ } のブロックが2度と実行されないようにしている。

次にnotifiersに蓄えられたnotifierオブジェクトに対してAddClient ( )メソッドで、cmd_NotifyAddClient ( )メソッドの引数で渡されたクライアントのobjectをnotifierと結びつける。

for (i = 0; i < pv->notifiers.size (); i++)
 pv->notifiers[i].AddClient (object);


そしてそのクライアントのオブジェクトもnoteClientsベクター配列に格納される。

pv->noteClients.push_back (object);
return LXe_OK;

以上からコマンドをUIコントロールにしたクライアントにイベントを知らせる仕組みをコマンドに実装するにはCLxBasicCommandを継承してコマンドクラスを作り、コマンドの属性全体のイベントに対してはbasic_Notifier ( )を、個別の属性に対してはatrui_UIValue ( )メソッドをオーバーライドして実装する必要があることがわかった。

それではまた来週。

カテゴリー別ページ



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

2011年08月25日

max script 勉強してみた その9 3dsmax 2012

前回、ややこしいオブジェクトのプロパティアクセスのコードの書き方を簡単に調べる方法があるって書いたけど、それはMAXScriptリスナーパネルのマクロレコーダーを使う方法だ。MaxScriptパネルはMAXScriptメニューのMAXScriptリスナーを選択すると表示される。

このMaxScriptリスナーパネルにはマクロレコーダーという機能が付いていて、これを有効にすると手動でMaxを操作した手順がコードになって表示される。マクロレコーダーを有効にするにはMAXScriptリスナーパネルでマクロレコーダーメニューの「使用可能にする」を選択して有効にすればいい(3dsmax2009以前のバージョンではマクロレコーダーをONにするとmaxのパフォーマンスが極端に落ちる欠点があったらしいので、2008とか使ってる人はずっとこれをONにしとかないように注意したほうがいいらしいよ)。

fig01

例えばBOXを作成して

fig06モディファイヤパネルでBOXの長さを50にすると、

MaxScriptリスナーの上側のポートに

$.length = 50

のように出力される。このことから$.lengthで長さのプロパティにアクセスできることがすぐにわかる。

上記のコードでオブジェクトを表わしているのは「$」の部分だけだ。これは現在選択しているオブジェクトを表わすものだったね。マクロレコーダーでは、このようにオブジェクトを示すのに現在の選択を使う方法と、パス名を使う方法の2通りの表示方法が選択できる。それがマクロレコーダーメニューの「明示的シーン オブジェクト名」と「選択に関連するシーン オブジェクト名」だ。上記の操作を「明示的シーン オブジェクト名」に切り替えてから行うと、

$Box001.length = 50

となる(ただし長さの数値が元から50の時は変化がないからコードは出力されないよ。別の数値を入力して試してみてね)。

ちなみにコードをマクロとして記録して再利用するなら「明示的シーン オブジェクト名」より「選択に関連するシーン オブジェクト名」の方が都合がいい。オブジェクトを名前で指定する方式だと同じ名前のオブジェクトにしかコードが適用できないからね。$だけならその時選択しているオブジェクトに対して同じコードで実行で可能だ。

こういう単純なものじゃなくてオブジェクトにモディファイヤを適用したようなものでもちゃんとコードが出てくる。

たとえばモディファイヤパネルでTwistモディファイヤを追加してから角度パラメータを変えてみるとこのようなコードが出る。

$Box001.modifiers[#Twist].angle = 21.5

これを見ればオブジェクトに適用されたモディファイヤがmodifiers配列からアクセスできるのが一目瞭然だ。「選択に関連するシーン オブジェクト名」に設定を戻せば

$.modifiers[#Twist].angle = 21.5

となる。

ただし選択が複数の場合はどっちの設定にしても選択セットを表わす「$」の形式のみになる。

コード出力の設定はまだ他にもある。

「絶対変換の割り当て」と「相対変換の操作」の選択は移動・回転・スケールの変換を変換結果を変換プロパティの値として代入する形式にするのか、それとも現在の状態からの変換操作を表わす形式にするのかを決める。例えば現在の位置が[100,200,300]にオブジェクトがある場合、これをZ方向に100移動させると、

「絶対変換の割り当て」なら移動後の位置[100,200,400]がプロパティposに代入されるコードになる。

$.pos = [100,200,400]

それに対して同じことを「相対変換の操作」にして行うと、

move $ [0,0,100]

となる。

回転ではこのような違いが出る(数値はドラッグして偶然出たもの)。上の方はクオータニオン回転データになってるな。

$.rotation = quat 0 -0.163632 0 0.986522

rotate $ (angleaxis -7.53628 [1,0,0])

スケールは移動と同じようにどちらもXYZの3つのパラメータで、元の大きさからの絶対スケールか、現在の大きさからの相対スケールかの違いがある。

$.scale = [1.16447,1.16447,1.16447]

scale $ [1.1161,1.1161,1.1161]

ただしこれも複数のオブジェクトを選択している場合は「相対変換の操作」の方のみの方式になるよ。

[明示的サブオブジェクト セット]と[選択に関連するサブオブジェクト セット] の切替は編集可能ポリゴンのポイントや面のようにオブジェクトを構成するサブオブジェクトについて、番号の配列で表わすのか、現在の選択セットとして表わすのかを切り替える。

例えば編集可能ポリゴンの頂点サブオブジェクトモードで

fig02

ポイントを複数選択して移動した場合、

fig03

[明示的サブオブジェクト セット] なら下のようにverts[#{頂点番号リスト}]として編集対象となる頂点番号を指定する形式になる。

move $Box001.verts[#{2, 4, 6}] [0,-1.48116,0]

[選択に関連するサブオブジェクト セット]なら単に現在選択されている頂点のセットということでselectedVertsとなる。

move $Box001.selectedVerts [0,-13.3767,0]

後ろの[0,-13.3767,0]の部分はmoveのパラメータでXYZ方向の相対的な移動距離だ。ちなみにサブオブジェクトの場合、単独の選択であっても「相対変換の操作」の方式でしかコード化されない。

こういうのを切り替えて見るだけでもコードで実際のオブジェクトをどう表現したらいいのかがわかってくるね。

また、デフォルトの状態ではコマンドパネルを切り替えたり、ツールやメニューを選択したりする動作はコードとして出力されないけど、マクロレコーダメニューの下の3つの項目にチェックを入れることでそれらの動作もコマンドとして出力出来るようになる。

fig04

ここで生成されるコードは

max <command_name>

って形式になっていて、「maxコマンド」と呼ばれるもので、スクリプト内から3dsMaxのメニューやツールバーのコマンドを操作できる。詳しくはマニュアルの「MAXScript ツールおよび 3ds Max との連携 > 3ds Max ユーザ インタフェースの操作 > 3ds Max コマンド > max コマンド」にあるよ。

コマンドパネルをモディファイヤパネルに切り替えたり作成パネルに切り替えたりなんて操作もこれでできる。

max modify mode モディファイパネルに切替

max create mode 作成パネルに切替

以上、マクロレコーダーさえあれば何でも簡単にコード化できそうに思えるんだけど、実際そうはうまく行かない。と言うのはリスナーにコードを出力するのはどうやらMaxの環境じゃなくて個々のツールがやっていることらしく、実装されてないものは全然出てこない。マニュアルにも

マクロ レコーダ出力が生成されない領域も多くあります

と書いてある。

それどころか、例えば「選択して移動」ツールを選んでステータスバーの座標入力の欄に数値を入力してもリスナーには何も出てこない。

fig05

だから結局マクロレコーダーを使ってコードを調べるこの方法も、万能ではないって事ね・・・。詳しい制限はマニュアルの「MAXScript の紹介 > MAXScript へのアクセス > マクロ レコーダ」の「マクロレコーダの制限」ってところに書いてあるよ。

それではまた次回。

maxまとめページ



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