MAX Script tutorial
Autor:


Na początek może kilka słów wyjaśnienia. Tutorial postanowiłem napisać w oparciu o jeden z moich skryptów - Rollin'. Służy on do tworzenia efektu toczącego się koła. Prościej mówiąc, obraca obiekt o kąt proporcjonalny do przbytej odległości. Po objaśnieniu działania niektórych linii pojawiają się dygresje (na szarym tle), które rozszerzają opisywany temat lub są krótkimi przykładami.

Pisząc ten tutorial przyjąłem założenie, że osoba czytająca go zna chociaż odrobinę jakiś język programowania (np. Pascal, C++), dlatego nie wyjaśniam takich pojęć jak zmienne, pętla itp. Jednocześnie proszę wszystkich fachowców od MAX Script'u o wyrozumiałość gdyż nie licząc dwóch lub trzech tutoriali jestem w tej dziedzinie kompletnym samoukiem. Zatem do rzeczy....


Wprowadzenie
MAX Script jak sama nazwa wskazuje jest językiem skryptowym czyli w przeciwieństwie do np.C++ bardzo uproszczonym. Napisane w nim skrypty można uruchamiać tylko pod MAX'em. Dodatkowo brak tu np.debuggera co niekiedy bardzo utrudnia pisanie. Jednak mimo swoich ograniczeń bywa bardzo pomocny, chociażby dlatego, że wiele zadań można zautomatyzować, oszczędzając sporo czasu.

Obok widzicie rozwiniętą zakładkę Utilities, tutaj rezyduje MAX Script.

Open Listener - przycisk ten otwiera okno MAX Script Listener'a. Wyświetlane są w nim wszystkie komunikaty, również te o błędach.

New Script - otwiera nowe, puste okno edycji skryptu.

Open Script - otwiera skrypt w oknie edycji.

Run Script - Kompiluje (lub jak kto woli wykonuje) skrypt.

Rozwijana lista Utilities - Tutaj pojawiają się nazwy wszystkich uruchomionych skryptów.
Od tej reguły są oczywiście wyjątki, np. skrypty które działają jako Render Effect lub Plugin. Wtedy pojawiają się jako nowy przycisk lub opcja.




Powyżej widzicie okno edycji skryptu. Większości opcji jest doskonale znana z innych programów. Jedyna nowość Evaluate All Ctrl+E służy do skompilowania skryptu. Z innych przydatnych kombinacji warto wymienić Ctrl+B, która służy do znajdywania wzajemnie domykających się nawiasów (bardzo przydatne, szczególnie w wielokrotnie zagnieżdżonych pętlach) oraz Ctrl+D która nadaje różnym fragmentom kodu różne kolory (bardzo poprawia przejrzystość).

Zaczynamy
	utility rollin "Rollin"
	(
Ta pierwsza linijka definiuje nowy skrypt. Po słowie utility znajduje się nazwa skryptu (zmienna pod którą skrypt będzie widoczny dla MAX'a) a w cudzysłowie nazwa która pojawi się w liście Utlities. Tutaj są one takie same, ale tak oczywiście nie musi być.

Jak już wcześniej wspomniałem nie każdy skrypt działa jako Utility. Poniżej znajduje się pięć pierwszych linijek bardzo popularnego skryptu HDRdomeLight.
plugin Geometry HDRdomeLight
name:"HDRdomeLight"
classId:#(0x89bf888f, 0x86ae823b)
extends:GeoSphere
category:"CSLights"
Oznaczają one mniej więcej tyle: skrypt pojawi się w zakładce Create/Geometry w nowej kategorii CSLights (wszystkie kategorie są w rozwijanej liście tam gdzie np. Standard Primitives). Będzie to przycisk z napisem HDRdomeLight. Linia extends:GeoSphere oznacza, że skrypt jest rozszerzeniem obiektu GeoSphere. Na podobnej zasadzie, czyli jako rozszerzenie działa skrypt SoftLight, z tą różnicą, że współpracuje on ze światłami. classId:#(0x89bf888f, 0x86ae823b) definiuje unikalny identyfikator pluginu.
Proste skrypty mogą się obejść bez interfejsu np. for i in 10 to 100 by 10 do (box position:[i*4,0,0] height:(random 10 100)). Wpisanie tej linijki i skompilowanie (Ctrl+E) spowoduje utworzenie 10 box'ów o losowej wysokości od 10 do 100. Proponuję żeby każdy kto zaczyna zabawę z MAX Script'em skopiował ją i spróbował jaki efekt dają zmiany poszczególnych parametrów, można np. uzależnić wysokość od zmiennej i, dodać zmianę rotacji obiektów.

Czas na zdefiniowanie interfejsu
	group "Settings"
	(
Poleceniem group tworzymy ramkę w której znajdują się poszczególne elementy interfejsu przyciski, spinnery itp. Nie jest to element konieczny ale pomaga elegancko rozplanować cały interfejs.
	pickbutton selobj "Select object"
Pickbutton pozwala wybrać pojedynczy obiekt (mesh, światło ,kamera itd.). Po naciśnięciu pozostaje wciśnięty dopóki czegoś nie wybierzemy.
	label lab_rotaxis "Rotation axis: " across:2
Element label pozwala umieścić w interfejsie dowolny tekst.
	radiobuttons rot_axis  labels:#("X","Y","Z") default:3 align:#left across:1
Radiobuttons jest elementem z kilkoma opcjami do wyboru (może być wybrana tylko jedna z nich), rot_axis jest zmienną pod jaką będzie widziany, natomiast "Rotation axis:" to nazwa jaka pojawi się w interfejsie. labels:#("X","Y","Z") definiuje możliwe opcje wyboru, default:3 ustawia domyślnie wybraną opcję, w tym wypadku wartość 3 oznacza oś Z czyli opcję trzecią, align:#left wyrównuje obiekt do lewej. W tym konkretnym skrypcie umożliwi on wybór osi obrotu.
Na pewno zwróciliście uwagę na opję across która pojawiła się przy ostatnich dwóch linijkach. Standardowo MAX Script umieszcza elementy interfejsu jeden pod drugim, opcja across pozwala na rozmieszczenie ich obok siebie.

W przypadku wyrównywania dostępne są trzy możliwości:
align:#left
align:#center
align:#right
Istnieją również inne polecenia definiujące wygląd elementów interfejsu:
offset - przesuwa element interfejsu o zadaną wartość wzdłuż osi X i Y np. button test "nazwa" offset:[10,12]
width i height - definiują szerokość i wysokość elementu np. button test "nazwa" width:90 height:50
pos - definiuje pozycję elementu np. button test "nazwa" pos:[10,15]

	spinner obj_radius "Radius: " type:#float range:[1,1000,48] fieldwidth:45 align:#left
Tworzymy pole w którym będzie można ustawiać żądaną wartość promienia obiektu. type:#float - rodzaj zmiennej (do wyboru: float, integer lub worldunits), range:[1,100,48] - ustawia kolejno: minimalną, maksymalną i domyślną wartość. fieldwidth:45 - definiuje szerokość tego pola (TYLKO pola a nie całego elementu).
	checkbox detect "Detect radius" enabled:false
Checkbox czyli pole które może być zaznaczone lub nie, zwracając odpowiednio wartość true lub false. Zaznaczenie pola uruchomi funkcję, która automatycznie wyznaczy promień obracanego obiektu.
	button center "Pivot to center of mass" width:140 enabled:false
Posłuży do umieszczenia Pivotu w środku masy animowanego obiektu. Ta opcja pojawiła się tutaj ponieważ w obiektach tworzonych przy pomocy modyfikatora Lathe Pivot Point znajduje się przeważnie gdzieś z boku i nie może być wykorzystany jako środek obrotu.
	checkbox reverse "Reverse direction" align:#left
Posłuży do zmiany kierunku obrotu.
	spinner calc_start "Calculation start: " type:#integer range:[1,1000,1] fieldwidth:47 align:#right
	spinner calc_end "Calculation end: " type:#integer range:[2,1000,100] fieldwidth:47 align:#right
Powyższe spinnery posłużą do określenia początkowej i końcowej klatki zakresu działania skryptu. Zwróćcie uwagę, że są one w przeciwieństwie do spinnera Radius typu integer (czyli liczbami całkowitymi tak samo jak numery klatek).
	spinner step "Every Nth frame: " type:#integer range:[1,10,2] fieldwidth:47 align:#right
Określa co ile klatek skrypt będzie tworzył nowy klucz. Jest to dość ważne, gdyż duża ilość kluczy potrafi skutecznie spowolnić pracę ze sceną.
	button go "GO!"
Przycisk który uruchomi skrypt.
	group "About"
	(
	label lab1 "Rollin'" align:#center
	label lab2 "by Adam Wierzchowski" align:#center
	label lab3 "E-mail: asd@grafik.3D.pl" align:#center
	label lab4 "Web: www.asd.3D.pl" align:#center
	)
Grupa zawierająca informacje o skrypcie.

To by było na tyle jeśli chodzi o interfejs skryptu Rollin, poniżej zamieszczam przykładowy skrypt który umieszcza w interfejsie wszystkie możliwe elementy. Kilka z nich zostało połączonych wzajemnymi zależnościami.
utility test01 "Elementy interfejsu"
   (
   bitmap ei01 bitmap:(bitmap 80 40)
   button ei02 "Przycisk"
   radiobuttons ei03 "Radiobuttons: " labels:#("Opcja 1","Opcja 2","Opcja 3","Opcja 4") columns:2
   checkbox ei04 "Checkbox" checked:true
   checkbutton ei05 "Checkbutton" 
   combobox ei06 "Combobox: " items:#("Opcja 1","Opcja 2","Opcja 3","Opcja 4","Opcja 5") height:6
   colorpicker ei07 "Colorpicker: "
   dropdownlist ei08 "Dropdownlist: " items:#("Opcja 1","Opcja 2","Opcja 3","Opcja 4")
   spinner ei09 "Spinner: "
   edittext ei10 "Edittext: "
   slider ei11 "Slider: " range:[0,997,123] ticks:5
   label ei12 "Label"
   listbox ei13 "listbox: " items:#("Opcja 1","Opcja 2","Opcja 3","Opcja 4") height:4 
   mapbutton ei14 "MapButton"
   materialbutton ei15 "MaterialButton"
   pickbutton ei16 "PickButton"
   progressbar ei17
   	button prog "Press me!"
   timer ei19 "Timer" interval:500
   
   on ei19 tick do (ei07.color = random [0,0,0] [255,255,255])
   on ei11 changed val do (ei12.text = val as string)
   on prog pressed do (for i in 1 to 1000 do ei17.value = 100.*i/1000)
   )
	on rollin open do
	(
Tutaj umieszczamy część skryptu która zostanie wykonana w momencie jego wykonania opcją Evaluate lub otwarcia rolety (Rollout).

Jak się zapewne domyśliliście MAX Script obsługuje również zdarzenie zamknięcia rolety. Przyklad poniżej.
utility test02 "Obsluga zdarzen"
(
on test02 open do
	(
	messageBox "Roleta zostala otwarta" title:"Uwaga !!"
	)
on test02 close do
	(
	messageBox "Roleta zostala zamknieta" title:"Uwaga !!"
	)
)
	global xvector, yvector,zvector
	global xradius, yradius,zradius
Deklaracja sześciu zmiennych globalnych. Zmienne xvector, yvector i zvector przechowują jednostkowe wektory które umożliwią zrzutowanie wszystkich punktów obiektu na płaszczyzny YZ, XZ i XY. W zmiennych xradius, yradius i zradius znajdzie się promień obiektu wyznaczony względem każdej z trzech osi.
	xvector = [0,1,1]
	yvector = [1,0,1]
	zvector = [1,1,0]
Nadajemy wartości zmiennym. Zapis xvector = [0,1,1] oznacza: zmiennej xvector zostaje przypisany punkt w przestrzeni o współrzędnych (0,1,1).
	)
Koniec obsługi zdarzenia on rollin open
	on selobj picked obj do
	(
Czas zrealizować obsługę PickButton'a "Select object". Po naciśnięciu i wybraniu obiektu zostanie on automatycznie przypisany do zmiennej obj. Może to być światło, kamera, mesh, NURBS cokolwiek. Ponieważ skrypt może obsłużyć tylko geometrię w dalszej częsci skryptu trzeba sprawdzić czy wybrany obiekt spełnia to kryterium.
	if (superClassOf obj == geometryClass) then
	(
Powyższa linia realizuje to o czym pisałem wcześniej, czyli sprawdzanie rodzaju obiektu. Zapis można rozszyfrować w ten sposób: jeśli superklasa obiektu obj to geometryClass wtedy wykonaj...
SuperClass to najbardziej ogólny podział wszystkich obiektów w MAX'ie. Są to np. światła, kamery, geometria, modyfikatory, materiały itd. W obrębie każdej superklasy istnieją bardziej szczegółowe klasy np.: geometryClass - Box, Cone, Editable_mesh, Nurbs, Sphere... light - Directionallight, freeSpot, Omnilight i tak dalej. Wymieniać można bez końca. Zachęcam do przestudiowania poniższego przykładu.

Skrypt utworzy kilka obiektów. Należy nacisnąć przycisk "Wybierz obiekt" i następnie wybrać jeden z nich. Zostanie wyświetlona jego superklasa i klasa.
rollout test03 "Klasy i superklasy"
(
on test03 open do
(
a=box()
b=OmniLight position:[-90,0,10]
c=Freecamera position:[-60,0,10]
d=Teapot position:[-30,0,10] radius:10
e=Dummy position:[30,0,10]
f=Helix radius1:5 position:[60,0,10]
)
pickbutton choice "Wybierz obiekt"

on choice picked obj do
(
string_info="SuperClass: "+(superClassOf obj) as string+"\nClass: "+(ClassOf obj) as string
messageBox string_info title:"Informacje o obiekcie"
)
)
rollfloater = newRolloutFloater "Test" 200 110
addrollout test03 Rollfloater
	global animated_object
Deklarujemy zmienną globalną w której będzie przechowywany animowany obiekt. Jak w każdym języku, tak i tutaj warto nadawać zmiennym czytelne nazwy będące jednocześnie opisem przechowywanej wartości (lub obiektu).
	animated_object = obj
Do zmiennej animated_object wpisujemy obiekt wybrany wcześniej przy pomocy PickButton'a.
	selobj.text = obj.name
Nazwa wybranego obiektu zostaje wpisana do PickButton'a. Powyższa linijka mogłaby wyglądać również tak: selobj.text = animated_object.name.
	detect.enabled = true
	center.enabled = true
	go.enabled = true
Ponieważ obiekt został wybrany, można uaktywnić elementy skryptu które odwołują się do niego (są to kolejno wyznaczanie promienia obiektu, ustawianie Pivota w środku masy i przycisk uruchamiający skrypt). Bez tego przypadkowe naciśnięcie jednego z nich przed wybraniem obiektu spowodowałoby komunikat o błędzie.
	detect.state = false
	)
	)
Odznaczamy checkbox'a "Detect radius". Jest to konieczne gdyż związana z nim funkcja mogła przechowywać promień poprzednio wybranego obiektu.
Koniec obsługi zdarzenia on selobj picked obj
	on go pressed do
	(
Najważniejsza część skryptu czyli to co dzieje się po naciśnięciu przycisku "GO!"
	animate on
	(
Polecenie animate on odpowiada naciśnięciu w MAX'ie przycisku Animate. Jak widać również w skryptach, jego "naciśnięcie" jest konieczne.
	cont = animated_object.rotation.controller
Przypisujemy kontroler rotacji obiektu animated_object do zmiennej cont. Kontroler to najprościej mówiąc zawartość pojedynczej ścieżki w oknie Track View. Mogą to być klucze, dane z systemu motion capture, expression i wiele innych rzeczy.
	for frame in animationRange.start to animationRange.end do
	(
Ta pętla zwróci kolejno numery wszystkich klatek w zmiennej frame. animationRange.start i animationRange.end odpowiadają wartościom Start Time i End Time w oknie Time Configuration.
Umożliwi to przeszukanie całego kontrolera rotacji klatka po klatce i skasowanie kluczy przed wstawieniem nowych.
	key_index = getkeyindex cont frame
Sprawdzamy jaki jest indeks klucza w aktualnej klatce animacji i wpisujemy go do zmiennej key_index. Jeśli taki nie istnieje do zmiennej zostanie wpisana wartość 0 (indeksy kluczy zaczynają się od 1).
	if (key_index) != 0 and (key_index) != 1 then (deleteKey cont key_index)
	)
deleteKey cont key_index kasuje klucz o indeksie key_index w kontrolerze cont. Nastąpi to tylko wtedy gdy indeks klucza jest różny od 0 (gdyż wtedy klucza po prostu nie ma, czyli nie ma czego kasować) i różny od 1 (skasowanie pierwszego klucza powoduje błędne działanie skryptu).
	for frame in calc_start.value to calc_end.value by step.value do
	(
Ta pętla będzie zwracała numery klatek w których skrypt wyznacza rotację i tworzy nowe klucze. calc_start.value zwraca wartość spinera "Calculation start" a calc_end.value spinera "Calculation end". step.value czyli wartość spinera "Every Nth frame" określa co ile klatek powstanie nowy klucz.
	if frame < step.value then (at time 0 (prev_pos = animated_object.pos))
		else (at time (frame-step.value) (prev_pos = animated_object.pos))
Instrukcja za else wyznacza pozycję obiektu w poprzedniej iteracji pętli, natomiast warunek za if jest zabezpieczaniem. Działa ono w momencie gdy np. "Calculation start" ma wartość 1 a "Every Nth frame" 4. Skrypt musiałby odwołać się do klatki -3 a takiej oczywiście nie ma, zamiast tego odwołuje się do zerowej. Pozycja obiektu zostaje wpisana do zmiennej prev_pos.
	at time frame (current_pos = animated_object.pos)
Wyznaczamy pozycję obiektu w aktualnej klatce animacji (zmienna current_pos).
	covered_distance = distance prev_pos current_pos
prev_pos i current_pos to punkty w przestrzeni 3D w których znajdował się i znajduje obiekt. Polecenie distance pozwala wyznaczyć odległość między nimi (tą którą przebył nasz obiekt).
	rotation = (180 * covered_distance)/(pi*obj_radius.value)
Teraz znając przebytą odległość covered_distance i promień obiektu obj_radius.value możemy wyznaczyć rotację rotation, zwykła geometria. pi to jest stałą zdefiniowaną w systemie, więc nawet nie trzeba znać jej wartości.
	in coordsys local at time frame
	(
Teraz bardzo ważna rzecz, przy pomocy in coordsys local przełączamy się na lokalny układ współrzędnych. Aby dokładniej zrozumieć o co chodzi proponuję otworzyć plik before.max (w pliku ze skryptem, Rollin.zip), zaznaczyć Torus01 i odegrać animację. Standartowo MAx ma włączony układ współrzędnych wievport'u. Najwyraźniej widać to w widoku Top, obiekty zakręcają a mimo to układ współrzędnych nie zmienia orientacji. Teraz proponuję zmienić układ na Local. Wyraźnie widać różnicę, oś Z cały czas pokrywa się z osiami torusów i właśnie o to chodzi. Teraz wystarczy odpowiednio wokół nich obrócić obiekty i efekt toczenia gotowy.
	case rot_axis.state of
	(
Instrukcja case...of umożliwi podjęcie różnych akcji w zależności od osi wybranej w "Rotation axis". rot_axis.state zwraca numer wybranej opcji.
	1: if reverse.checked == false then (rotate animated_object (eulerangles -rotation 0 0))
	    else (rotate animated_object (eulerangles rotation 0 0))
Na początek sprawdzamy czy została zaznaczona opcja "Reverse direction". Jeśli tak to rot_axis.state zwróci wartość true i zostanie wykonana instrukcja po then, w przeciwnym wypadku po else. Zapis rotate animated_object (eulerangles rotation 0 0) oznacza: obróć obiekt animated_object wokół osi X o tyle stopni ile wynosi wartość zmiennej rotation, a wokół osi Y i Z o zero stopni. Jeśli przed zmienną rotation pojawi się minus obrót nastąpi w przeciwną stronę.
	2: if reverse.checked == false then (rotate animated_object (eulerangles 0 -rotation 0))
	    else (rotate animated_object (eulerangles 0 rotation 0))
	3: if reverse.checked == false then (rotate animated_object (eulerangles 0 0 -rotation))
	    else (rotate animated_object (eulerangles 0 0 rotation))
	)
	)
	)
	)
	)
To samo dla osi Y i Z.
Następnie po kolei zamykamy pętle:
- case rot_axis.state of
- in coordsys local at time frame
- for frame in calc_start.value to calc_end.value by step.value do
- animate on
- on go pressed do

Skoro było o animacji proponuję dwa przykłady. W pierwszym, "Animacja kluczami", kulka animowana jest poprzez wstawianie kluczy do kontrolera i nadawanie im wartości. W drugim, "Animacja pozycją", pozycja kulki jest ustalana co kilka klatek przy pomocy właściwości position. Która metoda jest lepsza? Trudno powiedzieć, to jak zwykle zależy. Na pewno nie uciekniecie od kluczy w wypadku gdy trzeba będzie wielokrotnie modyfikować tor ruchu.
rollout test04a "Animacja kluczami"
(
button start "GO!"
label lab1 "Klatka: " across:2
label lab2 "" across:1
label lab3 "Pozycja X: " across:2
label lab4 "" across:1
label lab5 "Pozycja Z: " across:2
label lab6 "" across:1

on start pressed do
(
global kulka = geosphere radius:10 segments:4
cont = kulka.position.controller
licznik = 0
for i in 0 to 100 by 4 do
(
licznik += 1
addNewKey cont i
cont.keys[licznik].value = [70*sin(360/100.*i),0,70*cos((360/100.*2*i)+90)]
)
playActiveOnly = false
playAnimation()
)
)

fn current_frame = 
(
test04a.lab2.text = ((floor sliderTime) as integer) as string
test04a.lab4.text = (kulka.pos.x as integer) as string
test04a.lab6.text = (kulka.pos.z as integer) as string
)
registertimecallback current_frame
rollfloater = newRolloutFloater "Test" 200 150
addrollout test04a Rollfloater
Zwróćcie uwagę na kilka elementów. Po pierwsze konstrukcja as integer i as string, jest to zwykła konwersja typów. W skryptach nigdy nie jest ona dokonywana automatycznie. Po drugie registertimecallback current_frame, dzięki tej linijce funkcja current_frame zostanie wykonana za każdym razem gdy zmieni się aktualna klatka animacji, czy to na skutek wciśnięcia przycisku "Play Animation", czy na skutek przesunięcia suwaka "Time Slider".
rollout test04b "Animacja pozycja"
(
button start "GO!"
label lab1 "Klatka: " across:2
label lab2 "" across:1
label lab3 "Pozycja X: " across:2
label lab4 "" across:1
label lab5 "Pozycja Z: " across:2
label lab6 "" across:1

on start pressed do
(
global kulka = geosphere radius:10 segments:4
animate on
(
for i in 0 to 100 by 4 do
(
at time i kulka.pos = [70*sin(360/100.*i),0,70*cos((360/100.*2*i)+90)]
)
)
playActiveOnly = false
playAnimation()
)
)

fn current_frame = 
(
test04b.lab2.text = ((floor sliderTime) as integer) as string
test04b.lab4.text = (kulka.pos.x as integer) as string
test04b.lab6.text = (kulka.pos.z as integer) as string
)
registertimecallback current_frame
rollfloater = newRolloutFloater "Test" 200 150
addrollout test04b Rollfloater
Teraz czas na część kodu która przesuwa Pivot do środka masy obiektu. Konkretny punkt postanowiłem wyznaczyć obliczając średnią arytmetyczną położenia wszystkich Vertex'ów. Oczywiście wynik będzie prawidłowy tylko dla symetrycznych obiektów ale przecież wszystkie koła, walce itp. właśnie takie są.
	on center pressed do
	(
Jeśli przycisk center (czyli "Pivot to center of mass") zostanie przyciśnięty wykonywane są instrukcje po do.
	obj_center = [0,0,0]

Inicjalizujemy zmienną obj_center. Później posłuży ona do obliczenia środka masy.
	addModifier animated_object (edit_mesh())
Przypisujemy naszemu obiektowi modyfikator Edit Mesh. Umożliwi to dobranie się do poszczególnych vertex'ów.
	for i in 1 to animated_object.numVerts do
	(
Teraz rozpoczynamy pętlę która zwróci po kolei numery wszystkich vertex'ów. .numVerts to ich ilość w danym obiekcie.
	obj_center += getVert animated_object i
	)
Funkcja getVert animated_object i zwraca położenie vertex'a o indeksie i należącego do obiektu animated_object. Wszystkie pozycje sumujemy w zmiennej obj_center. Pamiętajcie, że jest ona typu Point3D zatem sumowanie wygląda np. tak [1,2,3] + [7,0,-1] = [8,2,2], dla każdej osi osobno. Analogicznie wyglądają wszystkie działania matematyczne na tym typie.
	obj_center /= animated_object.numVerts
Tutaj wyznaczamy średnie położenie punktu, czyli suma przez ilość vertex'ów. Jak słusznie zauważyliście dzielimy zmienną Point3D przez typ Integer. Wynikowy typ to oczywiście Point3D wygląda to np. tak: [8,16,2] / 2 = [4,8,1]. Czyli znowu wartość dla każdej osi została podzielona osobno.
	animated_object.pivot = obj_center
Przesuwamy Pivot obiektu w nowe położenie zdefiniowane w zmiennej obj_center
	deleteModifier animated_object 1
	)
Usuwamy modyfikator Edit Mesh. Jako ostatni nadany obiektowi modyfikator ma on indeks 1.
Żeby ułatwić wam zrozumienie indeksów modyfikatorów proponuję następujący przykład. Utwórzcie Box i przypiszcie mu kolejno Bend, Taper i Twist. Twist jako ostatnio przypisany i znajdujący się na szczycie listy modyfikatorów ma indeks 1, wcześniejszy Taper 2 i Bend 3. Jeśli skasujecie Twist'a indeks 1 otrzyma Taper który znajduje się teraz na szczycie a Bend dostanie 2.

Skoro w powyższej procedurze odbywały się operacje ma vertex'ach proponuję wam krótki przykład na ten temat. Po uruchomieniu skryptu wybierzcie dowolny obiekt. Zostanie wypisana całkowita ilość vertex'ów oraz pozycja jednego z nich wybranego przy pomocy spinnera "Numer"
rollout test05 "Operacje na vetexach"
(
pickbutton pick_obj "Wybierz obiekt" width:160
label lab1 "Liczba vertexow: " align:#left
spinner ver_numer "Numer: " type:#integer range:[1,100,1] align:#left enabled:false
label lab2a "Pozycja aktualnego:" align:#left
label lab2b "" align:#left

on pick_obj picked obj do
(
if (superClassOf obj == geometryClass) then
	(
	addModifier obj (edit_mesh())
	lab1.text = "Liczba vertexow: " + obj.numVerts as string
	ver_numer.enabled = true
	ver_numer.range = [1,obj.numVerts,1]
	lab2b.text = (getVert obj 1) as string
	deleteModifier obj 1
	)
)

on ver_numer changed val do
(
addModifier pick_obj.object (edit_mesh())
lab2b.text = (getVert pick_obj.object val) as string
deleteModifier pick_obj.object 1
)

)
rollfloater = newRolloutFloater "Test 05" 200 170
addrollout test05 Rollfloater 
Teraz czas na ostatnią funkcję realizowaną przez skrypt, czyli automatyczne wyznaczanie promienia animowanego obiektu. Na początek krótko opiszę zasadę działania. Wyobraźcie sobie kulę. Jej promieniem będzie odległość między środkiem a najdalej położonym vertex'em. Załóżmy, że chcemy obrócić ją wokół osi X. Wystarczy zrzutować wszystkie vertex'y na płaszczyznę ZY, czyli prostopadłą do osi X, wyliczyć średnią arytmetyczną ich położenia (która w większości wypadków pokrywa się ze środkiem obrotu) i znaleźć odległość do najdalszego vertex'a. To właśnie nasz promień. Oczywiście ta metoda nie jest doskonała. Efekt będzie prawidłowy tylko dla obiektów symetrycznych względem co najmniej dwóch osi. Czyli wszystkie kule, torusy, walce i obiekty uzyskane modyfikatorem Lathe zaliczają się do tej kategorii. I o to chodzi, skrypt przecież służy do toczenia obiektów, a trudno wyobrazić sobie toczący się Teapot (symetria tylko względem jednej osi).
	on detect changed new_state do
	(
Ta instrukcja jest wywoływana w momencie gdy zmienia się stan elementu "Detect radius". Jeśli został on zaznaczony zmienna new_state przyjmie wartość true, jeśli odznaczony - false.
	if new_state == true then
	(
Skrypt podejmie dalsze akcje jeśli "Detect radius" został zaznaczony, w przeciwnym wypadku nie zrobi nic.
	addModifier animated_object (edit_mesh())
Podobnie jak to miało miejsce w funkcji przesuwającej Pivot, nadajemy naszemu obiektowi modyfikator Edit Mesh aby dobrać się do poszczególnych vertexów.
	obj_center = [0,0,0]
Inicjujemy nową zmienną obj_center typu Point3D, nadając jej jednocześnie wartość [0,0,0]. W niej będzie przechowywana średnia arytmetyczna położenia wszystkich vertex'ów.
	for i in 1 to animated_object.numVerts do
	(
	obj_center += in coordsys local getVert animated_object i
	)
	obj_center /= animated_object.numVerts
Wyznaczamy średnią arytmetyczną położenia wszystkich vertex'ów. Analogicznie jak chwilę wcześniej w funkcji ustawiającej Pivot Point.
	f_xvert_index = 1
	f_yvert_index = 1
	f_zvert_index = 1
Inicjalizujemy trzy zmienne w których będą przechowywane indeksy najdalszych vertex'ów, odpowiednio dla każdej z osi. Na początek zakładamy, że najdalszym jest ten o indeksie 1.
	for i in 2 to animated_object.numVerts do
	(
Rozpoczynamy pętlę, która po kolei zwróci indeksy wszystkich vertex'ów aby można było sprawdzić ich odległość od środka obiektu. Jak zauważyliście rozpoczyna się ona od 2. Gdyby zaczynała się od 1 to w pierwszej iteracji sprawdzalibyśmy czy vertex o indeksie 1 nie znajduje się dalej od środka niż vertex o indeksie 1. Nie ma to żadnego sensu, lecz nie byłoby błędem.
	f_xvert_pos = in coordsys local getVert animated_object f_xvert_index
Odczytujemy pozycję najdalszego, znalezionego do tej pory vertex'a. Pamiętajcie, że nadal działamy w lokalnym układzie współrzędnych - in coordsys local.
	f_yvert_pos = in coordsys local getVert animated_object f_yvert_index
	f_zvert_pos = in coordsys local getVert animated_object f_zvert_index
To samo dla pozostałych osi.
	new_vert_pos = in coordsys local getVert animated_object i
Odczytujemy pozycję aktualnie porównywanego vertex'a. Jego indeks i zwracany jest przez pętlę for którą rozpoczęliśmy kilka linijek wcześniej.
	if (distance (obj_center*xvector) (new_vert_pos*xvector)) > (distance (obj_center*xvector) (f_xvert_pos*xvector)) 
		then f_xvert_index = i
Jeżeli odległość między obj_center zrzutowanym na płaszczyznę YZ a new_vert_pos na YZ jest większa od odległości między obj_center na YZ a aktualnie sprawdzanym punktem f_xvert_pos na YZ to indeks aktualnego punktu staje się indeksem punktu najdalszego. Uff, teraz to samo po polsku. Właśnie tutaj odbywa się rzutowanie o którym pisałem wcześniej. Załóżmy, że nasz obj_center ma współrzędne [7,-8,5], po rzutowaniu na płaszczyznę YZ wyglądałby tak: [0,-8,5]. Zatem wystarczy przemnożyć go przez inny punkt [0,1,1] czyli ów xvector (rozpisane działanie wygląda tak: [7,-8,5] * [0,1,1] = [7*0,-8*1,5*1] = [0,-8,5]). Gdybyście nie pamiętali to xvector, jak również dwa pozostałe punkty, został zadeklarowany na samym początku w części on rollin open do.
	if (distance (obj_center*yvector) (new_vert_pos*yvector)) > (distance (obj_center*yvector) (f_yvert_pos*yvector))
		then f_yvert_index = i
	if (distance (obj_center*zvector) (new_vert_pos*zvector)) > (distance (obj_center*zvector) (f_zvert_pos*zvector))
		then f_zvert_index = i
	)
To samo dla pozostałych osi.
	f_xvert_pos = in coordsys local getVert animated_object f_xvert_index
	f_yvert_pos = in coordsys local getVert animated_object f_yvert_index
	f_zvert_pos = in coordsys local getVert animated_object f_zvert_index
Ostatecznie wyznaczamy pozycję najdalszych vertex'ów dla każdej z osi. Ten krok jest konieczny na wypadek gdyby interesujący nas punkt został znaleziony w ostatniej iteracji zakończonej pętli.
	xradius = (distance (obj_center*xvector) (f_xvert_pos*xvector))
To co nas najbardziej interesuje, czyli wyznaczenie promienia. Odległość między zrzutowanym środkiem obiektu obj_center a znalezionym przed chwilą najdalszym vertex'em f_xvert_pos, również zrzutowanym.
	yradius = (distance (obj_center*yvector) (f_yvert_pos*yvector))
	zradius = (distance (obj_center*zvector) (f_zvert_pos*zvector))
I znowu powtarzamy tą czynność dla pozostałych osi.
	deleteModifier animated_object 1
Usuwamy już niepotrzebny modyfikator Edit Mesh.
	case rot_axis.state of
	(
	1: obj_radius.value = xradius
	2: obj_radius.value = yradius
	3: obj_radius.value = zradius
	)
	)
	)
W zależności od wybranej osi obrotu wpisujemy właściwą wartość do pola spinnera "Radius".

Na koniec kilka linijek, które zapewnią prawidłowe działanie interfejsu realizując zależności pomiędzy poszczególnymi elementami.
	on calc_start changed val do
	(
Instrukcja zostanie wywołana w momencie zmiany wartości spinera "Calculation start". Nowa wartość znajduje się w zmiennej val.
	if val > calc_end.value then calc_end.value = val
	)
Jeśli nowa wartość wprowadzona do spinera "Calculation start" (czyli val) jest większa od wartości "Calculation end" to "Calculation end" przyjmie tą nową wartość. W praktyce oznacza to, że "Calculation start" nigdy nie będzie większa od "Calculation end".
	on calc_end changed val do
	(
	if val < calc_start.value then calc_start.value = val
	)
Dokładnie to samo co powyżej tylko w drugą stronę. "Calculation end" nigdy nie będzie większa od "Calculation start".
	on rot_axis changed state do
	(
Instrukcja zostanie wywołana w momencie zmiany dokonanej w elemencie "Rotation axis" (radiobuttons). Nowa wartość jest przechowywana w zmiennej state.
	if (state == 1) and (detect.state == true) then obj_radius.value = xradius
Jeśli wybrano opcję pierwszą (state == 1) i element "Detect raduis" jest zaznaczony (detect.state == true), wpisz do spinera "Radius" wartość xradius.
	if (state == 2) and (detect.state == true) then obj_radius.value = yradius
	if (state == 3) and (detect.state == true) then obj_radius.value = zradius
	)
To samo dla pozostałych dwóch opcji.
	)
Koniec skryptu.

Gotowy plik znajdziecie tutaj. UWAGA: wciąż nie wiem dlaczego, ale czasami interfejs skryptów działających jako Utility nie pojawia się. W takim wypadku ściągnijcie najnowszą wersję, która otwiera własny rollout.

powrót
To by było na tyle. Mam nadzieję powyższy tutorial chociaż trochę wam się przydał. Jeśli macie jakieś sugestie bądź uwagi (nawet krytyczne) proszę o maila. Jeśli chcecie dalej zgłębiać tajniki MAX Script'u zachęcam do analizowania skryptów dostępnych na sieci, to chyba najlepszy sposób nauki (w moim wypadku okazał się skuteczny).
Copyright ©2002