W wersji 8.5 programu Autodesk Maya pojawiło się wiele nowych, ciekawych możliwości. Nowy mental ray, nowe niezwykle wygodne shadery z rodziny architectural, nowy system symulacji tkanin oraz wsparcie dla wiodącego (nie tylko w świecie oprogramowania graficznego) języka skryptowego – Pythona.
Większość skryptów pisanych dla Mayi, to niewielkie narzędzia, często tworzone przez jedną osobę. Natywny język skryptowy tej aplikacji graficznej – MEL, w zupełności wystarcza do pisania krótkich, zgrabnych kodów. Dlaczego, więc warto używać Pythona, skoro MEL oferuje wszystko czego potrzeba? Uważam, że są głównie dwa argumenty przemawiające na korzyść tego pierwszego, a są nimi: obiektowość i popularność. O obiektowości za chwilę, najpierw wyjaśnię dlaczego to, co powszechne jest dobre.
Oczywistym faktem, jest to, że im bardziej znany język, tym łatwiej znaleźć programistów posługujących się nim. W wypadku Pythona dla Mayi, fakt ten jednak nie gra dużej roli, bo implementacja jest dziwna i wymaga znajomości wewnętrznej filozofii działania programu. O wiele bardziej interesujące jest to, że inne wiodące aplikacje graficzne, takie jak na przykład: Houdini, Vue, Realflow czy Nuke, zapewniają wsparcie dla Pythona, co znacznie upraszcza przenoszenie danych między nimi.
Obiektowość nie jest oczywiście cechą niezbędną, do pisania skryptów dla Mayi, jednak znacznie ułatwia pracę w grupie, oraz pozwala na wygodne testowanie poszczególnych elementów kodu. Podejście obiektowe (informatycy niech mi wybaczą pobieżne wyjaśnienie tej kwestii) polega na napisaniu klasy, która będzie realizowała pewien zestaw funkcjonalności za pomocą wewnętrznych komend Mayi. Ułatwia to testowanie i ponowne wykorzystanie kodu.
W tym tekście pokażę jak wygodnie pisać i testować własne skrypty, wykorzystując obiektowe możliwości języka Python.
Zakładam, że znasz podstawy Pythona, gdyż w tym tutorialu nie będę przedstawiał jego składni. Jeśli nie miałeś wcześniej do czynienia z tym językiem, to najpierw przejrzyj tutorial dołączony do oficjalnej dokumentacji: http://docs.python.org/tut/ . Następnie warto by było przeczytać rozdział Python z dokumentacji Mayi, jednak nie będzie to niezbędne do zrozumienia tekstu.
Konfiguracja Mayi
Na początku musisz wyedytować plik Maya.env, który znajduje się w folderze \Moje dokumenty\maya\ tak, żeby zmienna PYTHONPATH wskazywała na główny folder ze skryptami pythona. Np.:
PYTHONPATH =C:\Documents and Settings\mtx\Moje dokumenty\maya\python
tam właśnie Maya będzie szukać skryptów.
Żeby sprawdzić czy wszystko ładnie działa załaduj Mayę i utwórz w swoim folderze Python, plik test.py:
def main():
print "test1"
Następnie ustaw w command line, język na Python (wystarczy kliknąć na nazwę, aby przełączać się między MEL-em i pythonem) i wpisz poniższe instrukcje (poprzedzone znakami >>>, których nie wpisuj):
>>>import test
>>>test.main()
test1
Konsola zgodnie z oczekiwaniami wypisze string "test1". Co jednak jeśli chcemy zmienić zawartość skryptu? Wyedytuj plik test.py tak aby wypisywał słowo "test2" zamiast "test1". Po wywołaniu funkcji test.main() maya znów wypisze wyraz "test1". Dzieje się tak, gdyż moduł test jest ładowany do pamięci i program nie wie, że zawartość pliku się zmieniła. Nic nie pomoże też ponowne napisanie: import test, gdyż już załadowany moduł nie zostanie załadowany powtórnie. Na szczęście po każdej zmianie nie trzeba wcale restartować Mayi. Wystarczy napisać reload(test), a moduł zostanie przeładowany. Najlepiej wpisz w script edytorze:
import test
reload(test)
test.main()
następnie zaznacz kod i przeciągnij na shelfa, to pozwoli na testowanie skryptu za pomocą jednego kliknięcia.
Podstawowe założenia skryptu
Teraz zabierzemy się za pisanie modułu implementującego obsługę czterech podstawowych kontrolek, bez których ciężko sobie wyobrazić interfejs: forma (Window), przycisk (Button), pole edycji (TextEdit) i etykiata (TextLabel). Każdy z tych obiektów ma pewne cechy wspólne, każdy ma rozmiar, widzialność i co najważniejsze unikatową nazwę stosowaną wewnątrz Mayi, coś jak uchwyt dla okien w WinAPI.
Klasa Window
Utwórz nowy plik w folderze Python np. o nazwie mxwindow.py (mam dziwne zamiłowanie do długich nazw, ale prawdziwie się mi to objawia, gdy programuję w C# :P). Na początku pliku dodaj linię:
import maya.cmds as cmds
aby skrypt zimportował moduł maya.cmds, dzięki czemu będą dostępne standardowe komendy Mayi. Słowo kluczowe as tworzy alias nazwy modułu, więc nie będzie trzeba pisać maya.cmds.columnLayout(adjustableColumn = True), a jedynie cmds.columnLayout(adjustableColumn = True). Oto schemat zależności klas, które będziemy pisać:
Założenie jest takie, że klasy Window, Button, TextLabel i TextEdit, są potomkami klasy Control. Kod klasy Control wygląda tak:
class Control(object):
"""Represents basic maya control. Shoud be used as abstract class."""
__melName = ""
__visible = True
__size = (0,0)
def getMelName(self):
"""Gets name representing control inside of Maya"""
return self.__melName
def setMelName(self, name):
"""Sets name representing control inside of Maya"""
self.__melName = name
def isVisible(self):
"""Checks whether control is visible"""
return self.__visible
def setVisibility(self, visibility):
"""Sets visibility of control"""
self.__visible = visibility
def getSize(self):
"""Gets size of control as (width,height)"""
return self.__size
"""Sets size of control (width,height)"""
def setSize(self, size):
self.__size = size
Klasa ta jest dość prosta, gdyż zawiera trzy prywatne atrybuty i akcesory do nich. W Pythonie istnieje ładniejszy system akcesorów, ale postanowiłem w tym tekście go pominąć i zrobić dostęp do atrybutów w stylu C++.
Jeszcze słówko na temat prywatnych atrybutów. Python nie zawiera żadnego mechanizmu ukrywania prywatnych atrybutów czy metod. Tutaj coś takiego nie istnieje i w gruncie rzeczy wszystko jest widoczne dla użytkownika klasy. Powołujemy się po prostu na rozsądek programisty, który naszego obiektu będzie używał, licząc na to, że nie zacznie babrać tam gdzie nie powinien. Zauważ jednak, że wszystkie prywatne atrybuty zaczynam od znaków __. W Pythonie istnieje mechanizm, zwany name mangling, który powoduje zmianę tak nazywanych zmiennych na postać: _NazwaKlasy__NazwaAtrybutu, co utrudnia (lecz nie uniemożliwia), dostęp do prywatnych danych.
Dodaj do tworzonego modułu funkcję:
def main2():
ctrl = Control()
ctrl.setMelName("real mel name")
ctrl.__melName = "fake mel name"
print ctrl.getMelName()
wypisze ona string "real mel name". Linia: ctrl.__melName = "fake mel name", nie jest błędem, gdyż po prostu stworzy nowy atrybut o podanej nazwie.
Pomiędzy znakami """ i """ znajdują się opisy działania metod. IDE interpretują je i wyświetlają jako podpowiedź przy pisaniu kodu.
Zgodnie ze schematem klasa Window dziedziczy po klasie Control:
class Window(Control):
"""Represents maya's basic window"""
name =""
controlsList = []
def __init__(self, name, size):
"""Initializes Window"""
self.setVisibility(False)
self.name = name
Control.__size = size
self.setMelName( cmds.window(title=name, widthHeight=size))
cmds.columnLayout(adjustableColumn = True)
def show(self):
"""Shows window"""
cmds.showWindow(self.getMelName())
self.setVisibility(not self.isVisible())
def hide(self):
"""Hides window"""
if self.isVisible():
cmds.toggleWindowVisibility(self.getMelName())
self.setVisibility(not self.isVisible())
def addButton(self, btnLabel):
"""Adds button to window"""
nuBtn = Button(self, btnLabel)
self.controlsList.append(nuBtn)
return nuBtn
def addTextLabel(self, txtLabel):
"""Adds text label to window"""
nuLabel = TextLabel(self,txtLabel)
self.controlsList.append(nuLabel)
return nuLabel
def addTextEdit(self, txtLabel):
"""Adds text edit to window"""
nuEdit = TextEdit(self,"")
self.controlsList.append(nuEdit)
return nuEdit
Jest to nieco bardziej skomplikowany twór, dlatego postaram się wyjaśnić jak działa. Klasa zawiera jeden atrybut - controlsList. Jest to lista kontrolek, który właścicielem jest okno (forma). Wprawdzie można by sobie poradzić bez niej, ale dobrze móc się odwoływać do kontrolek położonych na formie bez żonglowania dodatkowymi zmiennymi, np. w sytuacji, gdy chcemy wykonać jakąś akcję na wszystkich kontrolkach:
for ctrl in wnd.controlsList:
print ctrl.getMelName()
Klasa zawiera metodę __init__(). Jest to swego rodzaju odpowiednik konstruktora z języków C++ czy C#. Prawdę mówiąc, jest to funkcja wywoływana zaraz po konstruktorze (__new__()), ale doskonale nadaje się do inicjalizacji danych. Metoda przyjmuje dwa (poza standardowym - self) argumenty. Pierwszy to string określający tytuł okna. Drugi to tuple (=krotka, a fee), zawierający szerokość i wysokość okna. Najważniejszym fragmentem kodu tej metody jest:
self.setMelName( cmds.window(title=name, widthHeight=size))
cmds.columnLayout(adjustableColumn = True)
Pierwsza linia ustawia prywatny atrybut __melName, na wartość zwróconą przez mayową komendę window. Zapamiętanie melName’a jest niezbędne do późniejszego operowania na oknie.
Myślę, że warto teraz wspomnieć co nieco o konwencji wywoływania komend. W MEL-u jest to zazwyczaj:
nazwaKomendy –parametr1 wartosc1 –parametr2 wartosc2
w Pythonie:
maya.cmds.nazwaKomendy(parametr1=wartosc1, parametr2 = wartosc2)
.
Python pozwala na przekazywanie do funkcji nazwanych argumentów, dzięki czemu nie trzeba się trzymać ich kolejności i ilości. Konwencja nazewnictwa parametrów jest dokładniej wyjaśniona w helpie, pod Python\Python\Using Python (Maya2008/docs/Maya2008/en_US/wwhelp/wwhimpl/js/html/wwhelp.htm), ja wytłumaczę tylko to co niezbędne do zrozumienia przedstawionego kodu. Jednak najpierw wróćmy do opisu ciała metody __init__().
cmds.columnLayout(adjustableColumn = True)
ustawia typ layoutu okna, który umieszcza wszystkie kontroli w jednej kolumnie. Ustawienie parametru adjustableColumn na true, powoduje, że kontrolki należące do okna będą się wraz z nim rozszerzać i zwężać, trzymając się bocznych krawędzi.
Metody show() i hide() w prosty sposób wywołują komendy showWindow i toggleWindowVisibility, posługując się danymi klasy. Metody addButton(), addTextLabel(), addTextEdit(), tworzą egzemplarze odpowiednich obiektów i dodają kontrolki do okna. Sposób działania funkcji pokażę, na przykładzie pierwszej z nich.
def addButton(self, btnLabel):
"""Adds button to window"""
nuBtn = Button(self, btnLabel)
self.controlsList.append(nuBtn)
return nuBtn
Najpierw tworzony jest nowy egzemplarz obiektu klasy Button i referencja do niego jest dodawana do listy oraz zwracana przez funkcję. Po utworzeniu nowego obiektu wywoływana jest jego metoda __init__(), która dla przycisku wygląda następująco:
def __init__(self, parentControl, btnLabel):
self.setMelName(cmds.button(label = btnLabel,
parent=parentControl.getMelName()))
Komenda button, która tworzy nowy przycisk, wymaga podania nazwy okna, do którego przycisk ma zostać dodany. Dlatego konieczne jest przekazanie do funkcji obiektu okna.
Querying
Pewnie zastanawiasz się jak pobrać dane z kontrolki np. z TextBoxa. W MEL-u wykonuje się to za pomocą wysłania zapytania o wartość, czyli po prostu dodania flagi –q (query) przed nazwą parametru, którego wartość chcemy pobrać. W Pythonie też jest prosto. Oto metoda zwracająca tekst etykiety przycisku:
def getLabel(self):
"""Gets label"""
return cmds.button(self.getMelName(), q=True, label = True)
Jak widać wystarczy przesłać do funkcji dwa nazwane argumenty, jeden q, drugi nazwa_parametru i obu przypisać wartość True.
Dodawanie procedur obsługi zdarzenia
Przycisk do niczego się nie przyda, jeśli nie będzie reagował na kliknięcia. Metoda setOnCommand() ustawia procedurę obsługi kliknięcia.
def setOnCommand(self, procedure):
"""Sets onCommand procedure procedure_name(*args)"""
cmds.button(self.getMelName(), e=True, command = procedure)
Wykorzystuje do tego komendę button, z flagą edit. Podobnie jak w wypadku query, należy przekazać imienny argument e (edit) ustawiony na True, z tym, że tym razem chcemy wyedytować, a nie pobrać wartość, więc trzeba również przekazać argument z nową wartością.
Przykład użycia metody:
def tested2(*args):
print "tested2 executed!"
(...)
btn1.setOnCommand(tested2)
Procedury obsługi różnych zdarzeń, mogą pobierać różną ilość argumentów, dlatego najwygodniejszym rozwiązaniem jest dopuszczenie dowolnej ilości, przynajmniej w sytuacji, gdy nie będziemy z nich korzystać.
Pozostałe klasy pochodne
Pozostałe klasy dziedziczące po Control, są bardzo podobne do Button, dlatego nie będę ich oddzielnie omawiać. Mechanizmy, które zostały zaprezentowane powyżej, powinny wystarczyć do zrozumienia działania reszty kodu.
Cały kod, z przykładami użycia (funkcja main() ):
#!/usr/bin/env python
import maya.cmds as cmds
class Control(object):
"""Represents basic maya control. Shoud be used as abstract class."""
__melName = ""
__visible = True
__size = (0,0)
def getMelName(self):
"""Gets name representing control inside of Maya"""
return self.__melName
def setMelName(self, name):
"""Sets name representing control inside of Maya"""
self.__melName = name
def isVisible(self):
"""Checks whether control is visible"""
return self.__visible
def setVisibility(self, visibility):
"""Sets visibility of control"""
self.__visible = visibility
def getSize(self):
"""Gets size of control as (width,height)"""
return self.__size
"""Sets size of control (width,height)"""
def setSize(self, size):
self.__size = size
class Window(Control):
"""Represents maya's basic window"""
controlsList = []
def __init__(self, name, size):
"""Initializes Window"""
self.setVisibility(False)
Control.__size = size
self.setMelName( cmds.window(title=name, widthHeight=size))
cmds.columnLayout(adjustableColumn = True)
def show(self):
"""Shows window"""
cmds.showWindow(self.getMelName())
self.setVisibility(not self.isVisible())
def hide(self):
"""Hides window"""
if self.isVisible():
cmds.toggleWindowVisibility(self.getMelName())
self.setVisibility(not self.isVisible())
def addButton(self, btnLabel):
"""Adds button to window"""
nuBtn = Button(self, btnLabel)
self.controlsList.append(nuBtn)
return nuBtn
def addTextLabel(self, txtLabel):
"""Adds text label to window"""
nuLabel = TextLabel(self,txtLabel)
self.controlsList.append(nuLabel)
return nuLabel
def addTextEdit(self, txtLabel):
"""Adds text edit to window"""
nuEdit = TextEdit(self,"")
self.controlsList.append(nuEdit)
return nuEdit
class Button(Control):
def __init__(self, parentControl, btnLabel):
self.setMelName(cmds.button(label = btnLabel,
parent=parentControl.getMelName()))
def setOnCommand(self, procedure):
"""Sets onCommand procedure procedure_name(*args)"""
cmds.button(self.getMelName(), e=True, command = procedure)
def getLabel(self):
"""Gets label"""
return cmds.button(self.getMelName(), q=True, label = True)
def setLabel(self, btnLabel):
"""Sets label"""
cmds.button(self.getMelName(), e=True, label = btnLabel)
class TextLabel(Control):
"""Represents basic maya's text label"""
def __init__(self, parentControl, txtLabel):
"""Initializes maya's text label"""
self.setMelName(cmds.text( label= txtLabel,
parent = parentControl.getMelName() ))
def setLabel(self, btnLabel):
"""Sets label"""
cmds.text(self.getMelName(), e=True, label = btnLabel)
def getLabel(self):
"""Gets label"""
return cmds.text(self.getMelName(), q=True, label = True)
class TextEdit(Control):
"""Represents maya's basic text edit"""
def __init__(self, parentControl, initialText):
"""Initializes text edit"""
self.setMelName(cmds.textField( text= initialText,
parent = parentControl.getMelName() ))
def setText(self, newText):
"""Sets text in text edit"""
cmds.textField(self.getMelName(), e=True, text = newText)
def getText(self):
"""Gets text in text edit"""
return cmds.textField(self.getMelName(), q=True, text = True)
def tested2(*args):
print "tested2 executed!"
def main():
wnd = Window("test", (50,100))
wnd.addTextLabel("click to execute:")
btn1 = wnd.addButton("Click me!")
btn1.setOnCommand(tested2)
wnd.show()
for ctrl in wnd.controlsList:
print ctrl.getMelName()
print len( wnd.controlsList)
wnd.controlsList[0].setLabel("nu Label")