mel

Feb
4

Maya UI 裏的 Callback

GUI 是個非常偉大的發明,它讓電腦就這麼快速地滲透進家家戶戶與各大小商辦或公家機關,因為我們有 Mac OS、Windows、X Window System,再加上現下當紅的掌上型界面(iPhone、NDSL、PDS)與從 web browser 裏出生的 web interface,世界就此進入資訊高科技時代,人人都會使用這些電腦設備,因為它們都提供了一套容易使用的 GUI。

我依稀記得我的第一個有 GUI 的程式,是在新竹中學的軟研社22th招生影片)裏頭,使用 QuickBasic 呼叫底層的 VGA interrupt 來存取 video buffer,在螢幕上畫了兩個眼睛,裏頭的眼珠子會依據滑鼠游標所在的方位,往那對應的方向看過去。這隻程式除了滑鼠移動時所造成的 event 以外,就沒別的了,既沒有 button 也沒有 textField 供輸入什麼的,非常的陽春,但我寫得很愉快,這經驗一直是那麼的深刻。

時間拉回到進入太極影音以降。在 Maya 裏頭 scripts 提供 GUI programming 的函式,算是進入「MFC 之前,event driven 之後」的時代。所以 Maya 提供的 control(ex, button)大都可以針對一些 event(ex, click)指定一個 callback ,寫這類的程式對於一般修讀過四年資訊課程的學生來說,不是什麼問題。

遺憾的是,Maya 的 callback 機制很原始,callback 裏頭的程式碼並不會繼承所在的 namespace(or scope)的資訊,而是在 global namespace 下執行的,拿下頭這個例子來看:

proc dgCallbackCancel() {
    deleteUI "ooxx";
}

global proc dgShiftAnimationKeysGUI() {
    if(`window -exists "ooxx"`) {
        deleteUI "ooxx";
    }

    string $win = `window -title "ooxx by Drake" "ooxx"`;

    rowColumnLayout -nc 2 -cal 1 "right" -cal 2 "left" -cw 1 100 -cw 2 200 -w 300;
    text -label "Mode : ";
    string $mode = `radioButtonGrp -nrb 2 -labelArray2 "All" "Selected" -cw2 80 80 -select 1 "mode"`;
    text -label "Frames to shift : ";
    string $shift = `textField "shift"`;
    button -label "Do It" -c "dgCallbackDoIt()";
    button -label "Cancel" -c "dgCallbackCancel()";

    showWindow();
}

dgShiftAnimationKeysGUI();

兩個 button 的個別 callback functions 裏頭,為了可以順利存取到其它 control,使用了 hard-coded control name 的方式(直接假設 window name 是 "ooxx", 另外兩個 control 分別是 "mode" 與 "shift"),另一種作法是使用全域變數的方式,直接把這些 control 的名稱存到全域變數,然後在 callback functions 裏頭使用他們。

透過 pymel (0.7.8) 的包裝,加上 python 動態執行時仍擁有 local scope variables 的特性,可以既不用寫死 control name(寫死也沒關係,但已經沒有這個必要了),也不需要使用到全域變數,就可以做同與上頭一樣的事。

from pymel import *

def dgCallbackCancel(win):
    # just delete the window
    win.delete()

def dgCallbackDoIt(win, shift, mode):
    # do something and then delete the window
    # ...
    modeValue = mode.getSelect()
    shiftValue = shift.getText()

    win.delete()

def dgShiftAnimationKeysGUI():
    winName = "ooxx"
    if window(winName, exists=1):
        deleteUI(winName, window=1)
    if windowPref(winName, exists=1):
        windowPref(winName, remove=1)
    win = window(winName, title="ooxx by Drake")

    rowColumnLayout(nc=2, cal=[(1, "right"), (2, "left")], cw=[(1, 100), (2, 200)], w=300)
    text("Mode : ")
    mode = radioButtonGrp(nrb=2, labelArray2=["All", "Selected"], cw2=[80, 80])
    text("Frames to shift : ")
    shift = textField()
    button("Do It", align="center", c=lambda *args: dgCallbackDoIt(win, shift, mode))
    button("Cancel", align="center", c=lambda *args: dgCallbackCancel(win))

    showWindow(win)
dgShiftAnimationKeysGUI()

請仔細看 button 那兩行的最後。pymel(0.7.8)建議使用 lambda function 來包裝,然後依自己的需要把 local variables 給傳進 callback function 裏頭。所以在這個例子裏題頭,我可以把 window 與兩個 control(mode, shift)直接傳給 callback function,而不用去理會任何細節,同時很意外地減少了 programming side-effect。

pymel(0.7.8)對於 Maya GUI control 的包裝還非常的陽春,雖然每個 UI control 都包成一個 class,但用起來與直接呼叫 mel 無異,並沒有提供多少的抽像與方便的設計。舉個最明顯的例子:當你要對任一個 control 取值時,你得記得是使用 getText()、getSelect()、getLabel()、getValue()、getMaxValue()…多少有點強制要求 programmer 要熟習 mel UI programming 那一套,或是得很勤勞地查閱 api 說明文件或是利用 pydoc 之類的。但我想這一點在未來的發展會獲得改善。不過我也意外注意到,這問頭不管是古老的 MFC 還是 wxWidgets, Qt,也都一樣有這類的問題就是。我總覺得,讓使用者愈少去猜或計要呼叫哪個 member function,就是一個很好的設計 :p

Jul
5

如何從選取的節點中,過濾出燈光來?

Screenshot_Maya_retrieve_lights_from_selection

簡答如下:

from pymel import *
filter( lambda n: 'light' in n.classification(), ls(sl=True, dag=True, leaf=True))

這邊我使用 pymel 這個 python module 來簡化一些細節。

對照於文章後頭引用的原文(使用 mel 作法,建議先閱讀後,再回過頭來想上頭的這邊兩行程式碼),使用 pymel 不需檢查 getClassification() 的回傳值是不是空字串,透過 python 的 in 這個 operator 就會自動幫你處理掉一些「例外」了,同時,還可能因此少寫一些 if else。寫程式時,如果能少一些判斷式(if ... else ... elif ..., switch case ...),就「程式的易閱讀性」這一點來說,就像是少了一些特例的處理,是個比較好也比較推崇的寫法。像是說,日後在做維護時,不需要一個一個 if else 看下去,一個一個了解哪些情況下,要做什麼檢查之類的,因為對於正在維護或是新增功能的 programmer,他會希望花最少力氣就完成這類工作。同時,心理頭要能很有自信,確信不會因為他寫的程式而造成不必要的 bug 出來。少一些 if else,就少一些這層顧慮,因為你等於是替未來接手的 programmer(這個人常常是你自己)幫了個大忙。

同時,藉由 Functional Programming 裏好用的 list operator: filter ,原本要寫上四五行的 for loop + if else,現在只要一個 filter 就成了,當然,再加上隨寫即可用的 lambda function,寫起程式來,輕鬆得很。這裏的好處,不是只有把原來要寫四五行的程式碼縮短成一行而已,它同時暗指了,讓 programmer 去習慣把一個 for-loop 的想法,一個以流程為主的邏輯想法(flow-oriented logical thinking),轉化為以資料為主的想法(data-oriented thinking),而且是以比較高階的資料結構來思考(一次以一整個 array 或 list 來想)。

在 pymel 的 google groups 上頭,分別有兩位提供了不同的作法,列在內容供參考。

take out the 'sl=True' by using pymel's "selected()" function by Ofer Koren

from pymel import *
filter( lambda n: 'light' in n.classification(), selected(dag=True, leaf=True)) 

Doing it through the API requires more code but is considerably faster by Dean Edmonds

import maya.OpenMaya as om
sel = om.MSelectionList()
om.MGlobal.getActiveSelectionList(sel)
iter = om.MItSelectionList(sel, om.MFn.kLight)
lights = []

while not iter.isDone():
    light = om.MDagPath()
    iter.getDagPath(light)
    lights += [light]
    iter.next()

接下來的文章,是由 Bryan Ewert 所撰寫,原來可以在 http://www.ewertb.com/maya/mel 這個網址找到,但因為一些原因,這個網站已經關站一陣子了。很幸運的是,因為 Bryan 的 Mel Howto 寫得實在太好了,於是你可以透過這兩個網址,看到別人備份的版本:http://ewertb.soundlinker.com/ 或是 Wayback Machine。我把原文貼上來,方便看官你查閱。

Well, logically you would think this would do it:

string $selectedLights[] = `ls -sl -lights`;  

But -- while this does indeed return the list of selected lights, unfortunately it is only reliable if the user selects the shape node of the light. Often they won't, nor should they be required to. Fortunately, the "ls" command offers a convenient method for retrieving the shape nodes from the current selection:

string $selectedShapes[] = `ls -selection -dag -leaf`;  

By using this query, you need not concern yourself if the user has selected the transform or the shape for a particular node. From here, you can typically use the "nodeType" command to query if the shape is the type you're looking for:

if ( "mesh" == `nodeType $shape` ) {
  // Do something with a mesh shape. }  
if ( "camera" == `nodeType $shape` ) {
  // Do something with a camera. }  

However, "light" is not a node type; rather, there are several types of lights: spotLight, directionalLight, etc. A more generalized query is convenient in this case - that provided by the "getClassification" command. This command returns a classification string for the node's type. Be aware that this returns a string array and not just a string.

  getClassification spotLight;
 // Result: light //
  getClassification volumeLight;
 // Result: light //

The following procedure demonstrates its use for extracting all lights from the current selection.

proc string[] getSelectedLights()
{
  string $selectedLights[];

  string $select[] = `ls -sl -dag -leaf`;

  for ( $shape in $select )
  {
    // Determine if this is a light.
    // 
    string $class[] = getClassification( `nodeType $shape` );

    if ( ( `size $class` ) > 0 && ( "light" == $class[0] ) )
    {
      $selectedLights[ `size $selectedLights` ] = $shape;
    }
  }

  // Result is an array of all lights included in 
  // current selection list.
  return $selectedLights;
}
Syndicate content