2007年04月

2007年04月30日

ラティス変形のプログラムを作ってみよう その2 modo203

コ、コーヒーぎゅうにゅうがぁああああああああぁぁぁぁぁぁぁぁぁぁ・・・・・

パソコンをする時はコップの位置によく注意しようね\(^o^)/ さよなら、マウスとタブレット・・・・゚・(ノД`)・゚・。

さて、今回はラティス変形の原理について考えてみよう。自分が思うに、詰まるところ、1つの直方体内にぎっしり詰まった座標軸が、直方体の変形によってどう動くのかって問題に帰着するのではないだろうか。そこで下図のように、直方体内の点を直方体のエッジをベクトルと見立ててこれらのベクトルの合成で表現することを考えてみた。この場合、直方体の辺は互いに直行していて方向はX軸Y軸Z軸の3方向しか無いから直方体の1つの頂点を原点として表すとその点から3つの辺へ下ろした垂線によって決まる長さのX,Y,Z軸方向のベクトルの合成として表すことが出来る。しかし、この直方体を下図のように変形しても基本3ベクトルが乗っている3辺を変形させなければ3ベクトルは変化していないので点Pはこの変形に影響されない。これはちょっとまずい。この変形でもラティスの中にぎっしり詰まっている座標軸はねじれながらラティスの中に分布していくはずである。

3ベクトルのみで表現すると

そこで各辺が平行じゃないことを前提に考えてみると、ラティス内の座標軸も、平行じゃなくて、辺から辺へ長さと方向を変えて行っているようなものが想定できる。これをとりあえず2次元で考えると、辺から辺までの間で座標軸が比例配分で変化するとすれば、その軸と交差するエッジを比例分割して、その点を結ぶ線が座標軸になると考えられる。エッジをベクトルとして捉えると、比例配分はそのベクトルに0〜1の値をかけてやるのと同じだから、下図のようにp0p3というベクトルに対して0.2×p0p3としてやれば、p0からp3方向へ20%の距離を表現できる。

2ベクトルの合成

これなら媒介パラメータが0〜1まで連続的に変化した時、ベクトルもAからBへ滑らかに変化する。これを拡張して向かい合う辺全てに同じ媒介変数を適用すればそれによってラティスを比例配分的に切断する面が出来る。

12本のエッジでポイントを決める

この面は残念ながら平面とは限らないけど、その点を含む面も存在し、その面は媒介パラメータtによって決定する。しかしこのままではその点の位置は面上のどこかってこと以上にはわからない。そこで別のエッジについても新たな媒介パラメータt’、t’’を設けて同様の面を作ると、3面の交差によって3面が交わる点が1つだけ確定し、媒介パラメータ3つでラティス内の点の位置が特定出来る事になる。要するにラティスの8つの頂点がどんな位置にあっても中の点の位置は媒介変数t,t’,t’’の3つで表せて、その座標も計算出来るわけだ。これが自分のでっち上げたラティス変形の原理だ。たぶん他のラティス変形と同じような変形が出来ると思う。

そこでそれをファンクションにしてみよう。

まずはp1,p2の2点で表されるラティスのエッジ上の媒介変数tで表される点の座標を求めるファンクションの定義からだ。p1からp2へのベクトルはp2からp1を引き算すれば算出できる。

p2 - p1

このベクトルの長さに媒介パラメータtを掛けたものが点p1から目的の点Pまでのベクトルになる。

p - p1 = t ( p2 - p1 )

原点から点Pへのベクトルはp1ベクトルとそのベクトルの合成で表せるから、

p = t ( p2 - p1 ) + p1

実際のベクトル計算はx,y,z各要素ごと別々にこの計算をすればいいので、引数p1、p2にそれぞれx、y、zの要素がタプルの形で格納されているとしてファンクションを書くと以下のようになる。

def  ppos( p1, p2, t ):
    x= ( p2[0] - p1[0] ) * t + p1[0]
    y= ( p2[1] - p1[1] ) * t + p1[1]
    z= ( p2[2] - p1[2] ) * t + p1[2]
    return [x,y,z]

これを使ってラティスの8頂点と各軸の媒介パラメータt1,t2,t3から点Pの座標を算出するファンクションがこれだ。

def  newpos(p1,p2,p3,p4,p5,p6,p7,p8,t1,t2,t3):
    pp1=ppos(ppos(p1,p2,t1),ppos(p4,p3,t1),t3)
    pp2=ppos(ppos(p1,p4,t3),ppos(p2,p3,t3),t1)
    pp3=((pp1[0]+pp2[0])/2,(pp1[1]+pp2[1])/2,(pp1[2]+pp2[2])/2)
    pp1=ppos(ppos(p5,p6,t1),ppos(p8,p7,t1),t3)
    pp2=ppos(ppos(p1,p8,t3),ppos(p6,p7,t3),t1)
    pp4=((pp1[0]+pp2[0])/2,(pp1[1]+pp2[1])/2,(pp1[2]+pp2[2])/2)
    return ppos(pp3,pp4,t2)

1行ずつ説明していくと、

def  newpos(p1,p2,p3,p4,p5,p6,p7,p8,t1,t2,t3):

p1〜p8はそれぞれラティスの頂点の座標がタプルの形で入っている。t1〜t3は各軸の媒介パラメータだ。

    pp1=ppos(ppos(p1,p2,t1),ppos(p4,p3,t1),t3)

この式はラティスの底面の4点p1、p2、p3、p4を使って、点p1からp2への辺とp4からp3への辺をそれぞれベクトルとみなして媒介パラメータt1で点Pの各辺への投影点を出し、その2点を繋いだ辺をさらに媒介パラメータt3を使ってその辺への点Pの投影点を算出している。

式で求められる点

このpp1と同じようにして得られたラティスの上面の点を結んだ辺を媒介パラメータt2で内分した点が点Pになる。が、p1、p2、p3、p4は平面上にあるとは限らないので、このままだとt1の影響が強くなりそうなので、t3方向の辺p1→p4とp2→p3から同じ方法でpp2を算出して平均を取ってpp3としてみた。

    pp2=ppos(ppos(p1,p4,t3),ppos(p2,p3,t3),t1)
    pp3=((pp1[0]+pp2[0])/2,(pp1[1]+pp2[1])/2,(pp1[2]+pp2[2])/2)

pp3の式はpp1とpp2の各要素を足して2で割って、それを3つ並べてタプルに入れているだけだね。

    pp1=ppos(ppos(p5,p6,t1),ppos(p8,p7,t1),t3)
    pp2=ppos(ppos(p1,p8,t3),ppos(p6,p7,t3),t1)
    pp4=((pp1[0]+pp2[0])/2,(pp1[1]+pp2[1])/2,(pp1[2]+pp2[2])/2)

同様のことをラティスの上面の点p5、p6、p7、p8についても行って、pp4を算出。

    return ppos(pp3,pp4,t2)

上面と底面から算出したpp3とpp4から得られるベクトルを媒介変数t2で調整して点pの座標が得られ、それをreturnで呼び出し元に返している。

だから厳密に言うと、t2だけ特別軽い扱いをしているわけで、均一な変換とは言い難いかも知れないけど、実際にテストした結果ではこんなもんで充分なようだったので、この辺でお茶を濁すことにした。

以上でラティスの8頂点の座標とラティス内の点Pから各辺への投影点を決定するための媒介変数t1、t2、t3を決定できれば、ラティスが変形しても、それらを使ってラティス内の点の移動した位置を求めることが出来るわけだ。

点Pの座標値 → 変形前のラティスの頂点座標 → 媒介変数t1、t2、t3 → 変形後のラティスの頂点座標 → 変形後の点Pの座標値

要するに点Pをローカルラティス座標系の座標値t1、t2、t3で表しておけば、ラティスが変形してもその内部のローカル座標系では同じt1、t2、t3のままで、それをワールド座標系に戻せば変形位置が求まるって事だ。意味分かった?さらに書くとアイテム内に点を取って座標値を見ておいて、アイテムにスケールや回転、移動を加えても、その座標値はアイテム内のローカル座標値だから変化しないのと同じってことだよ。

それではまた次回。

It is no use crying over spilt coffee milk.

スクリプトまとめページ( Down Load はこちらから)  

カテゴリー別ページ

modo操作メモ
 



take_z_ultima at 13:34|この記事のURLComments(0)TrackBack(0)modo | CG

2007年04月29日

ラティス変形のプログラムを作ってみよう その1 modo 203

さて、今回からラティス変形を教材にPythonスクリプトの解説を続けて行くよ。一応ラティスはほぼ完成したのでゴールも見えてるし、コードも今のところ140行程度なので、勉強には丁度いい感じだよ。ここで言うラティス変形は自分の想像で作ったものなので、他のラティス変形と仕様が多少異なるかも知れないことを先に断っておくよ。

さて、まず仕様を定めよう。

  • X,Y,Z軸それぞれ任意の距離をもつ直方体を各軸に沿って等間隔に任意回数分割した時に出来るすべてのエッジを2ポイントポリゴンで置き換えたオブジェクトをラティスとする。
  • ラティスは1つのアイテムとして任意の位置に任意のスケールと回転角度で配置され、変形対象となるメッシュオブジェクトの子としてペアレントされる
  • ラティスはX,Y,Zのサイズと分割数を入力してスクリプトで生成する
  • ラティスへの変位は、相対モーフで与える
  • 変位を設定されたラティスが囲んでいる親のメッシュポイントは、ラティスの変位に比例して変形される

ラティス変形手順とりあえず今あるスクリプトでラティス変形の手順を示したのが左のGIFアニメだ。変形を受ける方のメッシュにモーフマップを施す必要は無いんだけど、モーフにしておくと、スクリプトを実行するたびに変形が累積されないで、いろいろ試すのにもってこいだ。完成したらそのマップをメッシュに「モーフを適用100%」で変形しちゃえばいいしね。



そのへんの操作はパネルを作ってやると良さそうだね。ワークフローは追って考えていくことにしよう。

さて、まずはアイテムの扱いからやっていこう。今回のスクリプトはアイテムを切り替えたり、アイテムのプロパティを元に座標変換したり、いろんなことをしなくちゃならない。そのための下準備だ。
以前にレイヤーについてやった時に各レイヤーを識別する方法としてレイヤーインデックスと言う通し番号があるって書いた。レイヤーを操作する時にはそのインデックスで指定するのは以前やったわけだ。そしてやっぱりアイテムも操作するには対象を指定しなくちゃならないから、レイヤーインデックスみたいなものが必要なわけだ。それがアイテムIDで、レイヤーインデックスが単なる通し番号だったのに対して、こっちは名前と他と重複しない適当な番号が合わさった文字列になっている。こんな感じだ。

mesh_0E537B340F16

実はレイヤーにもレイヤーIDって言うのがあって、取得してみるとアイテムIDと同じものが出てくる。たぶんこの2つは名前が違っても同じものなんじゃないかと思う。

現在選択されているアイテムについてアイテムIDを取得する方法はいろいろあるよ。一番手っ取り早いのは、アイテムを選択するコマンドselect.itemを使う方法だ。選択するコマンドで選択しているアイテムを取得するってのはちょっと奇異な感じがするかもしれないけど、そこにmodoのコマンドの面白い規則があるから、是非覚えてね。

select.item ?item:&item <mode:{set|add|remove|toggle}>

これはselect.itemコマンドのヘルプを出したときに出てくるSyntaxの記述だ。selectitemの後ろに「:」付きで書かれたitemとmodeはこのコマンドに与えられるコマンドパラメータだ。<>で囲まれているmodeの方は省略可能だ。必須はitemだけで、その前に「?」マークが付いている。これが重要。このようにコマンドの記述で?が付いているパラメータはクエリー可能なパラメータと言い、設定するだけじゃなくて設定されているものを逆に聞き出すことが出来るってことになっている。だからアイテムを選択するってコマンドでその選択するパラメータを「?」にすれば、選択しているアイテムを訊ねるってことになるわけだ。だから現在選択されているアイテムを得るには、

select.item ?

でいいわけだ。でもこれはクエリーできるパラメータがひとつの時だけで、正確には、

select.item item:?

と、「パラメータ名:?」という形でどのパラメータをクエリーするのかちゃんと書く。

それから、複数のクエリー可能なパラメータを持ったコマンドの場合、クエリーの対象じゃないパラメータも埋めておかないとPythonでは上手く値が取得できないみたいなので、「パラメータ名:ダミーデータ」という形で何でもいいから並べて書いとかなきゃならないようだ。クエリーで使う限りはこれらのダミーデータは無視されるので何でもいいよ。
もちろんスクリプトから使うには、lx.evalN()メソッドを使ってmodoにコマンドを送り込む必要があるし、選択されているアイテムは1つとは限らないからlx.evel()じゃなくてlx.evalN()の方がいいよ。

と、ここまで書いといてナンだけど、このコマンドには欠点があって、選択されているアイテムは何でもリストに入れちゃう。今回の場合、対象としたいのはメッシュアイテムだけで、テクスチャーだとか、ライトだとかカメラだとかは含めたくないわけだ。そんな時にアイテムを取得してから選別するのも大変だ。もちろんそういう方法も取れるけど、今回は専用のクエリーを使うことにした。

query sceneservice selection ? アイテムタイプ

これがそのクエリー。前にやったクエリーはlayerserviceに問い合わせていたけど、アイテムについてはscenserviceに問い合わせる。このクエリーはシーン内で選択されているアイテムをクエリー出来る。その時のパラメータとしてアイテムタイプを指定できるので、メッシュアイテムだけに絞って現在選択されているものを取得出来るわけだ。ファンクション化するとこんな感じ。

def selItems(itype):
    return lx.evalN("query sceneservice selection ? %s" % itype)

ファンクション化する程のモンじゃないけどね。ファンクションの引数itypeに入れられるパラメータは、 locator , mesh , light , camera , render , environment , textureLayer だ。これらを文字列データとして与えてやる。メッシュアイテムなら"mesh"ね。これで必ずアイテムIDはタプルに入った形で得られる。その中から必要な奴を[インデックス番号]を使って取り出せばいいわけだ。

このようにして得られたアイテムIDを使えばこのアイテムの位置、回転、尺度、なんかが簡単にクエリー出来る。

アイテムの座標

query sceneservice item.pos ? アイテムID

アイテムの回転角度

query sceneservice item.rot ? アイテムID

アイテムの尺度

query sceneservice item.scale ? アイテムID

アイテムの回転マトリクス

query sceneservice item.matrix ? アイテムID

アイテムの逆回転マトリクス

query sceneservice item.matrixInvrs ? アイテムID

アイテムの親アイテム

query sceneservice item.parent ? アイテムID

アイテムの子アイテム

query sceneservice item.children ? アイテムID

これらのクエリーとselect.itemコマンドを使って、今回のスクリプトはアイテム操作をして行くことになる。

今回のまとめ

  • コマンドパラメータの記述でパラメータ名に?が付いているものはクエリー可能である
  • コマンドパラメータの記述で<>で囲まれているパラメータは省略可能である
  • アイテムの識別にはアイテムIDを使う
  • 現在選択されているアイテムのIDを取得するには「select.item?」を使うのがお手軽だが、テクスチャなどの余計なアイテムも取得されてしまう事がある
  • 種類を絞り込んで選択アイテムを取得したければ、sceneserviceへのクエリーを使うとよい

それではまた次回

スクリプトまとめページ( Down Load はこちらから)  

カテゴリー別ページ

modo操作メモ



take_z_ultima at 13:36|この記事のURLComments(0)TrackBack(0)modo | CG

2007年04月28日

Pythonでスクリプト作ってみる?その12 modo 203

ゴールデンウィークにインターネッツなみなさんこんにちわ。
。゚・(ノД`)人(Д` )・゚。

さて、いよいよプログラムの完成だ。以下がそのコード。今回付け足したのは太字の部分だ。前回ループ選択が2股に分かれることがあるって書いたけど、このコードはそういうのが出てきたら、その経路については探索をやめるようにファンクションanalyze()を書き換えたよ。太字の部分だ。分岐があれば、ファンクションnextpoint()が複数のポイントを発見して、listで返してくるので判明する。それを使ってwhileのループを中断させ、その後の処理もスキップさせている。まあ対処療法って奴だね。

#python
import lx
def nextpoint(sp,loopset):
    neibor=set([str(p) for p in lx.eval("query layerservice vert.vertList ? %s" % sp)])
    neibor=neibor.intersection(loopset)
    loopset.remove(sp)
    if len(neibor)==0:
     return None
    elif len(neibor)==1:
     return neibor.pop()
    else:
     return list(neibor)
def analyze(v1,v2,verts):
    vset=set(verts)
    neibors=nextpoint(v1,vset)
    if type(neibors)!=list:neibors=[neibors]
    slength=None
    path=None
    for neibor in neibors:
        stream=[v1]
        stream.append(neibor)
        nv=neibor
        while nv!=None and nv!=v2 and type(nv)!=list :
            nv=nextpoint(nv,vset)
            stream.append(nv)
        if nv and type(nv)!=list and (slength==None or len(stream)<slength):
            slength=len(stream)
            path=stream
    return path
lx.eval("select.type vertex")
main=lx.eval("query layerservice layer.index ? main")
selpoint=lx.evalN("query layerservice verts ? selected")
if len(selpoint)==2 :
    nextp=set([str(p) for p in lx.eval("query layerservice vert.vertList ? %s" % selpoint[0])])
    loops=[]
    while(nextp):
        np=nextp.pop()
        lx.eval("select.element %s vert set %s" % (main,selpoint[0]))
        lx.eval("select.element %s vert add %s" % (main,np))
        lx.eval("select.loop")
        loop=lx.eval("query layerservice verts ? selected")
        nextp=nextp.difference(set(loop))
        if selpoint[1] in loop:
            loops.append(loop)
    path=None
    for loop in loops:
        seqp=analyze(selpoint[0],selpoint[1],loop)
        if path==None or len(seqp)<len(path):
            path=seqp
    lx.eval("select.drop vertex")
    if path :
        for v in path:
            lx.eval("select.element %s vert add %s" % (main,v))
    else:
        for v in selpoint:
            lx.eval("select.element %s vert add %s" % (main,v))
        lx.eval('dialog.setup error')
        lx.eval('dialog.title "選択エラー"')
        lx.eval('dialog.msg "2ポイントがループ上にありません"')
        lx.eval('dialog.open')

else:
    lx.eval('dialog.setup error')
    lx.eval('dialog.title "選択エラー"')
    lx.eval('dialog.msg "ポイントを2つ選択して下さい"')
    lx.eval('dialog.open')

さて、まず概要を示しておこう。プログラムの前半部分で変数selpointに探索経路の始点と終点のポイントインデックスがタプルの形で入り、変数loopsにそれら2点を通過するループがリストの形で格納されている。それを前提にして次のコードは始まる。既に1つのループ内の始点から終点への最短の経路はファンクションanalyze( )で得られるようになっているので、loopsに入っているループを1つずつ取り出して、analyze( )にかけて、各ループごとの最短経路を求めて、その中から一番短いものを見つけて、その経路上のポイントを選択すればミッション終了だ。それじゃあ追加した部分を順番に説明していこう。

path=None

変数pathは今まで見つかった中で最短の経路を記録するために用意した。最初は無いのでNoneで初期化しておく。

for loop in loops:

for文はloopsに入っているループを1つずつ取り出して変数loopにセットして、直下のプログラムブロックを実行する。これで全ループを総当りで処理するわけだ。

seqp=analyze(selpoint[0],selpoint[1],loop)

selpoint[0]に始点、selpoint[1]に終点、loopにループが入っている。それをファンクションanalize( )に渡して実行し、得られたそのループでの最短経路が変数seqpに入る。

if  path==None or len(seqp)<len(path):
    path=seqp

見つかった最短経路をどうするかは次の条件で決まる。

  • まだ他に最短経路が見つかっていない場合
     そのままそれを最短経路として記録
  • 既に最短経路が他に見つかっている場合
     過去の最短経路と新しく見つかった最短経路を比較して、新しい方が短ければ最短経路を更新する

要するに、変数pathに新しい最短経路を記録しなくちゃならないのは、

まだ何も見つかっていない場合と以前の最短距離より新しい最短距離の方が短い場合

これを条件式とすると、まだ何も見つかっていないのは、

path==None

新しい最短経路の方が短いって条件は、

len(seqp)<len(path)

前回のファンクションanalyze()では変数slengthに経路のポイント数を記録して比較したけど、今回はlenファンクションを使って比較する都度ポイント数を計算させている。本当は統一した方がエレガントだね。計算量はslengthを使ったほうが少ないし、プログラムとしてはこっちの方がスッキリしている気もするね。
この2つの条件のどちらかが成立していたら、新しい最短距離が一番短いものとして記録しなおす。だから条件はorで繋いで、どっちか成り立ったときってなるわけだ。

path==None or len(seqp)<len(path)

この条件が成り立っている時にpathに新しい最短距離を代入するわけだ。

path=seqp

ここまでの事を全ループにわたって繰り返せば、ループの中で最短のものが変数pathに代入されるわけだ。いよいよ最後はその経路をポイント選択する。

lx.eval("select.drop vertex")

ポイント選択をする前に、ループ選択とかで既にどこかがポイント選択された状態になっているはずなので、select.dropで選択を全解除しておく。

if path :
    for v in path:
        lx.eval("select.element %s vert add %s" % (main,v))

最短経路がひとつでも見つかっていれば変数pathにはそれが入っている筈で、何も見つからなければ最初に代入されたNoneが入ったままになっている筈である。そしてNoneは条件式としては偽(False)として評価されるので、このif文は最短経路が見つかった場合を真(True)として直下のプログラムブロックを実行する。
その実行されるブロックはfor文で、pathに含まれているポイントインデックスをひとつずつ取り出して、変数vに代入しては直下のselect.elementコマンドを実行することで、pathに設定されている経路上のポイントを全て選択するわけだ。

else:
    for v in selpoint:
        lx.eval("select.element %s vert add %s" % (main,v))
    lx.eval('dialog.setup error')
    lx.eval('dialog.title "選択エラー"')
    lx.eval('dialog.msg "2ポイントがループ上にありません"')
    lx.eval('dialog.open')

if文が偽(False)だった時はこのelse直下のプログラムブロックが実行される。最初のfor文はスクリプト実行前に選択されていた始点と終点の2ポイントを選択しなおして、ポイントの選択状態をスクリプトの実行前の状態に戻す。これは別にやらなくてもいいんだけど、こうしておけば、何が原因でエラーが起きたか、ユーザーが分かりやすいという効果がある。
続いてエラーダイアログを定義して表示して終わりだ。

これでようやく最初の目標にたどり着いた。下のGIFアニメは3通りの経路で選択してみた例だ。ポイントが8つの経路が選択されている。

使用例

2点間のポイントが順に取れたわけだから、このルーチンをベースにいろんなことが出来るはずだよ。例えば直線状に並べるとか、円弧状に並べるとか、両端のウェイトマップ値で間のポイントのウェイト値をグラデーションさせるとか、等間隔にするとか、ポイントに付いてる法線方向を便りに両隣のポイントとの成す角を記録して別のポイント列に適用するとか、いろいろ出来ると思うよ。
今回は2ポイント選択して経路を求めたので、経路の指定に曖昧さが残ったけど、もっと多くの基準点を与えて経路探索させるようにすれば、ループ1つじゃ取れない経路も選択可能になるね。そういうプログラムに書き換えるのは意外と簡単だよ。選択されているポイントを繋がっている順に2ポイントずつ取り出して、今回のプログラムにかけて、2点間の最短経路を求めて、それらを繋いでいけばいいね。直方体のエッジなんてループが止まっちゃうから、通過点として角を選択するなんて事も出来るようになるよ。あーそれから、プログラムの中からプログラムを呼び出す事は意外と簡単に出来て、今回のスクリプトがsample142.pyだとすると、lx.eval("@sample142.py")って書いてやれば、スクリプトの中からスクリプトを呼び出せる。もちろんsample142.pyが実行できるフォルダに入ってないとダメだけどね。

完成したスクリプトはアップしておいたよ。

それではまた次回。

スクリプトまとめページ( Down Load はこちらから)  

カテゴリー別ページ

modo操作メモ



take_z_ultima at 12:10|この記事のURLComments(0)TrackBack(0)modo | CG

2007年04月27日

Pythonでスクリプト作ってみる?その11 modo 203

いよいよゴールデンウィーク到来だな。自分は人望が無いのと友達がレジャー産業関係者が多いのもあって、この時期誰からも声がかからない・・・・゚・(ノД`)・゚・。 毎年予定も無いまま突入して、野菜の苗を買ってきて植えて、草むしりして終わりみたいな感じだな。ちょっと前に「えいご漬け」を買ったから、英語でもがんばろうかな。今の自分の評価は旅行で困らないレベルになったよ(おー出世じゃ!)。

前回のまとめを書くのを忘れていたのでまずは前回のまとめから。

  • ループ上の隣接ポイントを探索するのにset集合とset集合のintersection演算を使うとお手軽
  • プログラムは機能をまとめて名前をつけてファンクションとして部品化できる。
  • ファンクションの定義はプログラムの冒頭で行い、次のようにする

    def ファンクション名(引数,・・・):
       プログラム本体
           :
       return 呼び出し側に戻す値(省略可)
  • ファンクションは何をインプットして何をして何を出力するかで捉える。インプットはファンクション名の後ろの()内に書かれた引数に対して行う。出力はreturn文に書く。return文に書いた戻り値は実行時にファンクションの記述と置き換わって処理される。だから戻り値のあるファンクションは式の中に混ぜて使うことが出来る。

以上。

今回は前回作ったファンクションを使ってループ上の始点から終点への最短経路を探す方法についてやっていこう。最短経路と書いたのは、ループがその言葉の通りリング状であれば、始点と終点を繋ぐ経路は2通りあるから、そのうちの両点間のポイント数の少ない方を採用しようって事だ。探索に使うループはスクリプトの前半部分として以前に解説した通りだ。スクリプトを実行すると、リストに何本かが蓄積される。これらのループに対してそれぞれ経路探索をして、その中から最短のものを探し出すことになるので、繰り返し行われるループ上の始点終点の最短経路の探索についてファンクション化するのが良さそうだ。そこで作ったのが以下のファンクションだ。

def analyze(v1,v2,verts):
    vset=set(verts)
    neibors=nextpoint(v1,vset)
    if type(neibors)!=list:neibors=[neibors]
    slength=None
    path=None
    for neibor in neibors:
        stream=[v1]
        stream.append(neibor)
        nv=neibor
        while nv!=None and nv!=v2 :
            nv=nextpoint(nv,vset)
            stream.append(nv)
        if nv and (slength==None or len(stream)<slength):
            slength=len(stream)
            path=stream
    return path

順を追って説明していこう。

def analyze(v1,v2,verts):

まずファンクションの定義だ。()内に書かれた引数v1に探索の始点、v2に終点、vsetsにループデータが入る。点は全てポイントインデックスを文字列で表したもので、ループデータはループ上の全てのポイントをコンテナ(setでもlistでもtuppleでもOK)に入れたものだ。

vset=set(verts)

vertsに入っているループデータをset集合に変換して変数vsetに代入する。今後はこのsetに対して探索をして行く事になる。

neibors=nextpoint(v1,vset)

さっそく前回作ったファンクションnextpoint( )が使われている。v1には探索の始点になるポイントが、vsetにはループが無加工なまま入っている。ループが途中で切れていて、その端に始点が来ていない限り、始点の隣のポイントは2つ存在するので、変数neiborsには始点の両サイドの2ポイントのリストが代入される。始点がループの端なら隣のポイントは1つしか無く、その場合は隣のポイントのインデックス番号がそのまま代入される。このあたりはちょっとめんどくさい事しちゃった感があるけど、どこかにしわ寄せって来るものだから諦めてくれ。

探索の最初の段階

ここでファンクションnextpoint( )は隣のポイントを見つけるだけのものじゃなかったのを思い出してくれ。あのファンクションにはループから基点に指定したポイントを削除する作用も持たせていた。だからこの1行でループの中から始点が削除されるよ。次の段階で、今取得した隣のポイントを基点としてnextppoint( )を呼び出せば、始点が消えているから、隣として見つかるのは経路の進行方向のものだけだ。

隣のポイントが1つだけになる

そしてその基点も削除される。こうして、1ステップ探索するたびに探索し終わったループは端から消えていく。

if type(neibors)!=list:neibors=[neibors]

このif文はループの端に始点が来てしまって、隣のポイントがリストではなく、そのままのデータでneiborsに代入されちゃった時に、それをリストに入れた形に直す働きをする。変数neiborsにどんなタイプのデータが入っているかはtype()ファンクションを使えば簡単にわかる。type(neibors)!=list は変数neiborsの中身のタイプがリストではなければ真(True)になると言う条件式だ。そして、[neibors]はこのような使い方をするとneiborsの中身を要素とするリストになり、それを改めて変数neiborsに代入しているから、中身が単なるポイントインデックスだったのが、ポイントインデックスを要素とするリストになっちゃうわけだ。こんな事しなくちゃならないのは、次に出てくるfor文がneiborsをリストとして使うことを前提に書かれているから、結果を揃えなくちゃならないわけだ。だったらそんな事にならないようにファンクションnextpoint()の方を書き換えれば良さそうなもんだけど、このファンクションが使われる時の大半は一方向にしかポイントが無い時で、そのたびにリストからポイントを抜き出すのもわずらわしいのでこうしちゃった次第だ。あんまりエレガントな書き方じゃないね。

slength=None

変数slengthは見つけた経路の中で一番短い経路のポイント数を入れておくためのものだ。最初は見つけたものが無いからNoneを入れておく。そして最初に見つかった奴を入れなおす。次に見つかったものからは、記録された長さと見つけた経路の長さを比較して、新しく見つけた経路のほうが短ければ過去に見つけた経路は破棄して新しい経路を記録し、この変数も書き直す。これを繰り返せば最終的に一番短い経路だけが残るって言う計画だ。

path=None

この変数は発見した中で一番短い経路を覚えておくためのものだ。最初はひとつも無いのでNonenを入れておく。

for neibor in neibors:

始点から終点へのループ上の経路は最大2方向考えられるので、for文でそれぞれの方向に対して探索するようにする。あらかじめ始点の両側のポイントをneiborsに入れて、始点を削除してあるので、neiborsに入っているポイントを始点にして探索すると、それぞれ逆の方向に辿っていけるはずだ。このfor文で繰り返される範囲は一番下のreturn文のひとつ上の行までだ。for文直下の行からそこまでの行で、変数neiborに入っているポイント方向に経路探索して見つかった時は前に見つけたものと比較して短い場合に記録を更新するというプログラムが入っている。for文はこの作業をneiborsを切り替えながら見つかったポイントの数だけ繰り返し実行するわけだ。

stream=[v1]
stream.append(neibor)

経路は変数streamにリストの形で格納する。目下のところ、始点のv1、続いてその隣のポイントの2つのポイントは経路上に順に並んでいることが分かっているので、最初にリストに追加しておく。

nv=neibor

変数neiborはfor文で使っているのでループ中に書き換えるのはまずいので、変数nvにコピーしておく

while nv!=None and nv!=v2 :
    nv=nextpoint(nv,vset)
    stream.append(nv)

while文は条件式が真(True)のうちは、直下のプログラムブロックを繰り返し実行する仕組みだったよね。このwhile文の条件式はnv!=Noneとnv!=v2の2つあって、and演算子で結合されている。and演算子は日本語に直すと「〜かつ〜」だったよね。要するに両方とも真の時だけ真。変数nvには新しく発見された経路上のポイントが入ってるんだよね。そして、ループが途中で途切れていた場合は次のポイントが無いので、nvはNoneになる。そうしたらもうそれ以上その経路を探索する必要が無いので、nvの値がNoneになった時点でwhileのループを終了する。v2には終点のインデックスが入ってるんだったよね。だからnvがv2になったらもう探索の必要が無いのでwhileのループを終了する。それらどちらの条件になってもこの条件式は偽(False)になってwhileの繰り返しは終了するわけだ。
そして、繰り返されるプログラムブロックの方は、ファンクションnextpoint()を使って、最後に見つかった経路上のポイントnvと探索に使ったポイントは除外してあるループが入ったvsetから、次のポイントを見つけ出して変数nvを更新し、かつ、vsetから古い方のnvを除外する。そして新しく見つかったポイントを経路としてstreamに入っているリストにappend()メソッドを使って追加する。
これら一連の動作によってwhile文が終了した時点で、stremaには、始点v1から、終点v2までのポイントが経路の順に格納されるか、それともv2が見つからないままループが途切れてそこまでのデータになったものかが格納される。

if nv and (slength==None or len(stream)<slength):
    slength=len(stream)
    path=stream

while文で探索し終わるとここに来る。何度も書くけどこの時点で得られているであろう探索結果は次の通り。

  • 経路が見つかった
  • 経路が見つかる前にループが途切れた

ここでしなくちゃならないことは、ちゃんとした経路が発見できていた場合、過去に発見された経路があれば、それと距離を比べてそれより短い時に最短経路として記憶しなおすことだ。
だから見つからなかった場合はそのままスルーでいいわけだね。この2つの状態を見分ける方法はいくつかあるけど、while文の条件式から作り出すのが簡単だ。なんてったってwhileのループが終了したのはその式の条件が整わなくなったからだからね。経路が見つからなかった時、最後に変数nvに入るのは、Noneになる。Noneは偽(False)として扱えるんだったよね。だから条件式としてnvだけ書いておくと、nvにNoneが入っているときは偽(False)になって、何か入っている時は真(True)として使えるよ。「nv!=None」って書いたほうが読み易いからそっちの方がオススメだけど、今回は書き方のバリエーションを見せる意味であえてこんな書き方にしてみたよ。if文中の条件式nvは他の条件とandで結ばれているから、nvが真(True)にならない限り他の条件がどうであろうとも式は偽(False)になるね。これで、経路が見つからなかった場合はスルーする部分は表現できた。
次に経路が見つかった場合だけど、過去に見つかった経路と比較って書いたけど、最初の段階では何も記録が無いよね。それは現在発見されている最短経路を記録しておく変数pathの値がNoneであること、または最短経路内のポイント数が入ったslengthがNoneであることで確認できる。その時は、最初に見つかった経路をとりあえず現在の最短経路として記録しちゃえばいい。それが、条件式の「slength==None」の部分なんだね。逆に経路として記録されたものがあるなら、それと今見つかった経路との距離を比較して、前の奴より短かいかどうか確かめるのが、「len(stream)<slength」の部分だ。新しく見つかった経路亜streamに入っているからlen()ファンクションで要素数を求めればそれが経路のポイント数ってことになる。過去のポイント数の記録はslengthに入っているから、それより少ない場合が真(True)となればいいわけだ。
以上の条件式を日本語に直すと、

「新しい経路が見つかり、かつ、初めて見つかった経路であるか、または、過去の経路より短い経路であれば」

ってことだ。orで結んだ条件式が()で囲まれているのは()内の条件式の方を先に計算しろって事だよ。それから条件式の順番は一見どうでもいいように思えるけど、実はそうでもない。式は左から順に評価されるので、評価の途中で条件が確定してしまう場合は、それ以降の式を評価しないことになっている。だから条件によっては評価不能なものは、右の方に書いて、評価を後回しにして、その式を評価するための条件が必ず満たされる場所に書くことにする。そうじゃないと、条件によって実行中にエラーで止まったりしちゃうからね。今回の場合はそういう部分は無いけどね。
if文の条件式が真(True)になったら変数slengthとpathを新しいものに更新する。ここまでが先に出て来たfor文の繰り返すプログラムブロックだ。あのfor文は始点の隣に見つかるポイントを1つずつ取り上げて、見つかったポイント全ての方向に経路を探索させるための繰り返しだった。だからこのif文もforの繰り返す回数分だけ繰り返されて、結果として最短の経路だけが変数pathに残るわけだ。

return path

for文が終了するとこのreturn文にたどり着く。この時点で見つかった最短経路を呼び出し側に戻してやるのがこの文の役目だ。

さあ、これであるループ内での2点間の最短経路が求められるようになったよ。と、思ってた矢先になんと、ループが途中で分岐する現象を発見。

ループが分岐する例


工エエェェ(´д`)ェェエエ工工
み、見なかったことにしよう・・・。
とりあえず問題は先送りにしてプログラムの完成を優先させることにした。
次回はいよいよプログラムの完成だ。たったこれだけのことでも、説明すると長くなるね。

今回のまとめ

  • プログラムはその機能を単位として、「何を入れて、その結果何が出てくるか」として捉えると把握しやすいし、そういう単位で完結するように書くといい
  • ループ選択が分岐することがある

今回は新しく出て来た事柄が無いのでとくにまとめる内容もないか・・・。

それではまた次回。

スクリプトまとめページ( Down Load はこちらから)  

カテゴリー別ページ

modo操作メモ



take_z_ultima at 12:33|この記事のURLComments(0)TrackBack(0)modo | CG

2007年04月26日

Pythonでスクリプト作ってみる?その10 modo 203

前回でスクリプトの前半部分である、2ポイントを通過するループのリストを取得するところまでやった。後半はその中から2ポイント間のポイント数が一番少ない経路を探し出して選択するわけだ。残念ながらループ選択したからと言ってポイントが順番に並んでいる保証は無いので、得られたループを1つずつ辿って見るしかない。ちょっと考えると嫌気が差しそうな作業だけど、これも例のsetを活用すると、意外と簡単に作れるよ。

隣のポイントまず、ループ内の隣のポイントを見つける方法を考えてみよう。隣と聞いてピンと来た人はちゃんと勉強が進んでる人だね。例のvert.vertListのクエリーを使えば、あるポイントに隣接するポイントが全て得られるんだったよね。それらのポイントのうち、ループ選択したポイントに含まれているものを探せば、ループ上の隣接ポイントって事になるでしょ。さて、隣接ポイントの集合であり、かつ、ループ上のポイントの集合を求めるには?2つの集合の共通部分を取り出すintersection()メソッドを使えばいいね。



ループ上のある点に隣接する点=隣接点の集合.intersection(ループ上の点の集合)

ただ、このままだと両隣のポイントが得られる。一方向に辿りたいなら、探索が終了したポイントはループポイントの集合から削除して行けばいい。ループポイントの集合に入っていないポイントはintersectionしても出てこないからね。

さて、だんだんプログラムが長くなって来て、見通しが悪くなりつつある。そんな時はプログラムの機能を分割して、まとめるようにした方がミスも起きにくいし、生産性も高まるよ。Pythonではプログラムをまとめる方法のひとつとして、ユーザー定義ファンクションという方法がある。特定の機能を持ったプログラムコードに名前をつけて定義し、そのプログラムが使いたい場所でその名前を書いて呼び出す方法で、まるで最初からその機能がPythonに組み込まれているかのようにプログラミング出来るようになるものだ。例えばループポイントの集合の中のあるポイントの隣のポイントを求めるプログラムをファンクションとして定義して置けば、その後のプログラムはその機能を使うことだけ考えて作業できるし、それを使ったプログラムは元々のプログラムコードが挿入されるよりはスッキリして可読性がよくなるはずだ。定義はこんな形でプログラムの先頭に書く。

def ファンクション名(引数,・・・):
  プログラム本体
      :
  return 呼び出し側に戻すデータ(必要なければ省略できる)

ファンクション名の後ろの()にある引数リストは、このファンクションを実行する時に与える初期値を入れるための変数で、このファンクションを呼び出す側でここに書いた変数と同じ数の初期値を与えなくちゃならない。ファンクションは何らかの処理をするだけの場合もあれば、処理結果をファンクションの値として呼び出し側に戻す場合もある。値を戻したい時はreturn文を書いて、その後ろに戻したいデータを書いて置けばいい。
例えば2つの数値を合計するファンクションadd()を定義するとこうなる。

def   add( n1 , n2 ) :
    return n1 + n2

このファンクションを呼び出すにはこのように書く

sum = add( 10 , 20 )

今まで出て来た他のファンクションやメソッドと何ら違いは無い。ファンクションが実行される時は、()の中に書かれた10と20が定義の()内に書かれた変数n1とn2に順番に代入されて、defの次の行から順に実行され、この場合、returnの行が実行されるので、n1+n2が計算されて合計の30に置き換わり、それをreturn で呼び出し側に戻すことで、add(10,20)が30に置き換わって変数sumに代入される。使う側から考えると、「add()ファンクションは2つの値をセットして呼び出すと、合計が戻ってくるものだ」という事だけ考えておけばいいね。ファンクションは一度作っちゃえばプログラムをブラックボックス化出来るメリットもあるわけだ。

ファンクションの定義が分かったところで、ループ上の1点から隣の点を求めるファンクションを作ってみると、こうなる。

def nextpoint( sp , loopset ) :
    neibor=set([str(p) for p in lx.eval("query layerservice vert.vertList ? %s" % sp)])
    neibor=neibor.intersection(loopset)
    loopset.remove(sp)
    if  len(neibor)==0:
        return None
    elif  len(neibor)==1:
        return neibor.pop()
    else:
        return list(neibor)

後からこのファンクションを使う都合で、return文のところがちょっと複雑になっているね。順に説明していくよ。

def  nextpoint( sp , loopset ) :

まずファンクションの宣言をしている行だ。重要なのは()内に書かれた2つの変数だ。このファンクションが呼び出される時に呼び出し側からデータを受け取るための2つの変数が定義されている。変数spには、探索の始点となるポイントのインデックス番号が文字列の形で、変数loopsetには、そのポイントを通過するポイントループのインデックス番号が文字列の形で入ったタプルが渡されることを想定している。だからこのファンクションを呼び出すときには、これら2つの変数に代入するためのデータを用意して、nextpoint(ポイントインデックス,ループのタプル)として呼び出さないとダメだよ。

neibor=set([str(p) for p in lx.eval("query layerservice vert.vertList ? %s" % sp)])

この行は、変数spに入っているポイントインデックスから隣接ポイントを取得するクエリーを実行し、得られたタプルをfor文で1つずつ取り出して変数pに入れてstr(p)を繰り返し実行することでタプル内のインデックス番号を全て文字列に変換し、それをリスト化する。それをさらにset()でset集合に変換して変数neiborに代入している。だから変数neiborには変数spのポイントに隣接するポイントの集合が代入され、その要素は全て文字列データで表されている事になる。

neibor=neibor.intersection(loopset)

この式でループの集合と隣接ポイントの集合の重なった部分の集合を作って変数neiborに代入しなおす。ループ内に隣のポイントが存在していれば、変数neiborに1つか2つの要素を持った集合が入る。無ければ空っぽの集合が入る。

loopset.remove(sp)

調査が終了したら基点となったポイントはループポイントの中から除外しておく。そうすることで、次にこのファンクションを呼び出して隣のポイントを求める時に、調査が逆戻りすることを防いでいる(自分の隣の隣がまた自分になる場合があるでしょ。隣を見つけた時点で自分が消えればその隣は向こう側の隣だけになるって理屈だ)。

if  len(neibor)==0:
    return None
elif  len(neibor)==1:
    return neibor.pop()
else:
    return list(neibor)

前出のif文の説明のところで省いていたけど、if文にはelifと言う仕組みもあって、ifとelseの間に何個でも挟むことが出来て、if文の条件式が偽(False)だった時に、さらにそこから条件が絞り込めるようになっている。上記の例では、「neiborの要素数が0だったら、Noneを返す。そうじゃなくて、neiborの要素数が1ならば、neiborから要素を1つ取り出して戻す。それも偽(False)だったらneiborをリストにして戻す」って事になる。
何でいちいちリストにしているのかって言うと、集合setは順番が無いので、中から要素を1つだけ取り出したりするのが苦手だからだ。リストなら[インデックス番号]をつければ簡単に要素にアクセスできるモンね。
こういうファンクションが完成したら、もう中身のことは忘れて、呼び出す時の方法と機能、戻ってくる値だけを把握しておけば良くなる。

呼び出し方法:nextpoint( 隣接ポイントを調べたいポイント , ループポイントのタプル )

機能:ループポイント上で指定ポイントの隣のポイントを調べる

戻り値:ポイントが見つからない場合はNone。1つ見つかったらそのポイントのインデックス。2つ見つかったらポイントのリストを戻す。

さあ、上記のような機能を持ったファンクションを使って始点からループを辿って終点までの間のポイントを見つけ出そう。そのためには次の手順でいいだろう。

  1. 始点の両隣のポイントを見つける
  2. ループから始点は除外しておく
  3. 1で見つけたポイントを始点としてループを辿って隣のポイントが無くなるか、終点が見つかるまで、隣の点を見つけ続ける。その際に見つかった点は全て順に記録しておく。
  4. 見つけた経路が終点まで繋がっていたら、見つけた中で一番短い経路を得る

これもまたファンクション化しておくといいだろう。プログラムの前半部分で始点終点の2点を通過するループが幾つか見つかっているはずだから、それらに対して1回ずつこのファンクションを実行して、何本かの経路を見つけ、そこからさらに最短の経路を選ばなくちゃならないから、この作業も繰り返し使うわけだね。

だいぶ長くなったので、この続きは次回にしよう。

ラティスの方はなんとなく弄ってこんな感じのところまでは作ったよ。それで思ったんだけど、ラティス内部の点って仮想するんじゃなくて、実際に存在する点にしちゃっても構わないんじゃないかな?どうせレイヤも分かれているので内部に点があっても修正中に変形させるメッシュのポイントを選択しちゃう事も無いし、予想外の変形することも無いもんね。もしかして、他のラティスもそうなってるのかな?ほぼ想像で作っているのでハッキリしないんだけどね。写真見ただけで作ったタクアンがコーヒー味だったりって事は世の中には結構あるからねwww。 今のところプログラムはたったの95行だよ。あと残っているのは、ラティスを生成するプログラムと、親子関係になっているメッシュとラティスの間での座標変換かな。今のところラティスは親レイヤーと同じ座標系じゃないとならないけど、座標変換をつけてやれば、任意の方向を向いたラティスで変形が可能になる。そろそろ公開しろって?

lattice

それではまた次回。

スクリプトまとめページ( Down Load はこちらから)  

カテゴリー別ページ

modo操作メモ



take_z_ultima at 12:07|この記事のURLComments(0)TrackBack(0)modo | CG
Archives