Grafika 3D
 
  Zarejestruj się
::  Newsy  ::  Pliki  ::  Twoje Konto  ::  Forum  ::
Menu
· Strona główna
· Forum
· Linki
· Lista u?ytkowników
· O nas...
· Pliki
· Statystyki
· Twoje Konto
Tutoriale
· API
· Matematyka
· Teoria
· Direct3D
· OpenGL
· Techniki
Kto Jest Online
Aktualnie jest 35 gość(ci) i 0 użytkownik(ów) online.

Jesteś anonimowym użytkownikiem. Możesz się zarejestrować za darmo klikając tutaj
Tutoriale - Techniki - 3D Max Exporter, czę?ć druga

W poprzednim tutorialu odwaliliśmy kawał dobrej roboty - "napisaliśmy" szkielet plugina, odpaliliśmy 3DMax-a i stwierdziliśmy, że wszystko to pięknie działa jak na razie. Niestety jeszcze nie dowiedzieliśmy się jak to wszystko ruszyć z miejsca, żeby wyglądało całkiem użytecznie i no i żeby dało się z tym jakoś współpracować. Czas więc na kontynuację, która pozwoli nam wydobyć z 3DMax-a wszystko, co nam będzie potrzebne do naszego programu. Otwieramy więc poprzedni nasz projekt i przyglądamy się mu dokładniej.

W poprzednim tutorialu w zasadzie nie włożyliśmy wielkiego wysiłku w napisanie naszego plugina, wiec tym razem będziemy musieli się trochę jednak postarać. Zaczniemy oczywiście od zapoznania się z naszym kodem źródłowym. Otwieramy nasz projekt, przechodzimy do widoku drzewka klas (użytkownicy VC oczywiście) no i cóż tam widzimy. Dwie klasy - jedna o nazwie ExportClassDesc dziedziczaca z klasy ClassDesc2 oraz druga - ta ważniejsza, czyli Export - tak sobie ją nazwaliśmy a w zasadzie pozwoliliśmy, żeby wizard w VC ją tak dla nas nazwał. Dziedziczy ona po (o dosyć obiecująco brzmiącej nazwie) klasy - SceneExport. Tak naprawdę zajmiemy się tylko naszą drugą klasą, bo pierwszej nie powinniśmy w ogóle ruszać - ona zawiera tylko kilka funkcji inline i to jednolinijkowych, potrzebnych do tajemniczych operacji 3Dmaxowi i nic nie wnosi do naszych rozważań, za to dosyć dokładnie przyjrzymy się klasie numer 2. Jak każde porządne SDK tak tez i dostępne w 3Dmaxie zawiera coś takiego jak dokumentacje - znajdziemy ją oczywiście po zainstalowaniu w menu lub we właściwym katalogu. Niestety w przypadku 3DMax-a dokumentacja ta ma pewna denerwującą wade - otóż jest... całkowicie niezrozumiała i na pierwszy rzut oka zupełnie nieprzydatna! Przyznam szczerze, że widziałem już wiele dokumentacji w swoim programistycznym życiu, ale czegoś takiego jak to nie miałem okazji poznać. Zawiera to oczywiście opis tego co trzeba, ale w taki sposób, ze więcej trzeba się nagłowić i naszukać niż z niej pożytku jakiego. Ale jest także i druga strona medalu... otóż jako jedni z nielicznych autorzy SDK dołączyli pewien miły dodatek do tejże dokumentacji - fragmenty z forum, na którym użytkownicy 3DMax SDK wymieniają się pytaniami i doświadczeniami nabytymi podczas pisania pluginow a że większość z nich to użytkownicy zdecydowanie początkujący wiec to przyda nam się bardziej nawet niż sama dokumentacja. No a już w parze staną się całkiem znośnym źródłem informacji.

No ale wróćmy do naszego kodu - klasa Export a co za tym idzie i SceneExport. Zaglądając do dokumentacji dowiemy się, że jest to klasa bazowa do napisania naszej własnej, służącej do przeprowadzenia eksportu sceny na nasze potrzeby. Zawiera ona kilka metod, które pozwolą nam tak przystosować nasz plugin, żeby był bardziej użyteczny niż dotychczas - po prostu trzeba będzie nadpisać kilka jej metod aby to wszystko zadziałało jak trzeba.

Po pierwsze - pamiętamy z poprzedniego tutorialu obrazek, który przedstawiał otwarte okno do eksportu i z listą możliwych formatów do wyeksportowania. Był tam też i nasz format, tyle że charakteryzował się zupełnym brakiem rozszerzenia jeśli chodzi o plik. Ponieważ jesteśmy porządni to jednak załóżmy, że wolelibyśmy mieć takie rozszerzenie dla naszego pliku - chcemy? To mamy - wystarczy spojrzeć albo do dokumentacji albo do kodu źródłowego, pod warunkiem, że włączyliśmy generowanie komentarzy w ostatnim okienku konfiguracyjnym wizarda - jak widać na przykładzie opłaca się czasem taki komentarz sobie wygenerować.

Więc metoda Ext() klasy SceneExport. Jej działanie jest tak naprawdę dosyć skomplikowane, ale zakładając, że my będziemy obsługiwać tylko jeden rodzaj plików eksportowalnych za pomocą tego plugina jej kod będzie wyglądał banalnie prosto:
const TCHAR* Export::Ext( int n )
{
  return _T( "rob" );
}
Funkcja po prostu zwróci nazwę rozszerzenia - jako parametr przyjmuje ona indeks rozszerzenia które ma zwrócić. Po co i na co to komu - przyznam, że nie rozumiem potrzeby, ale nam wystarczy wiedzieć jedno - jeśli chcemy aby okienko eksportu nam się zmieniło i nasze pliki otrzymywały domyślnie jakieś rozszerzenie to funkcja ta zwraca bez względu na argument przyjmowany jedyną wartość - czyli nasze pliki zyskają rozszerzenie *.rob na cześć swojego twórcy ;)).

Dalej - widzimy w okienku exportu, że prawie wszystkie inne rozszerzenia mają jakieś opisy, mówiące o tym, co to za rodzaj. Chcemy? Nic trudnego - oczywiście metoda LongDesc() - jak wynika z komentarza i opisu. Tutaj nie ma mieszania z parametrami - po prostu zwracamy opis i już.

Podobnie się można bawić innymi metodami - ja je nazywam porządkowymi. Jeśli wszystko wypełnicie i zorganizujecie jak trzeba możecie na przykład po naszych działaniach dostać coś takiego:


Tak można sobie oczywiście upiększać nasz plugin bez końca, ale przecież nie o to nam chodzi. My chcemy mieć w pliku zjadliwą dla naszego nowego, genialnego engine geometrię, która wreszcie będzie można rozstrzelać za pomocą zaprojektowanej w tym samym programie nowej, super giwery ;).

Więc już nie nudźmy więcej tylko przystąpmy do działania... Oczywiście pod naszą lupę idzie metoda o jakże słodziutko brzmiącej nazwie, czyli DoExport(). Pobiera ona szereg argumentów i widzimy, że zawiera ona jakiś tajemniczy kod, który powiem szczerze możemy z czystym sumieniem na razie wyciąć (albo i na stałe). Jest to kawałek powodujący wywołanie małego okienka z nazwiskami autorów wizarda, którego użyliśmy jakiś czas temu do stworzenia kodu, na który właśnie patrzymy. Okno to jak zdążyliśmy już zauważyć pojawia się po próbie zapisu czegoś za pomocą naszego plugina. Wiemy także, że max zgłaszał potem błąd, że coś tam się źle wykonało - w postaci małego komunikatu. Aby więc pozbyć się i tego, zmieniamy metodzie DoExport() wartość zwracaną z FALSE na TRUE i voila - już plugin zachowuje się o wiele, wiele lepiej. Teraz po próbie zapisania czegoś w naszym pliku okienko eksportu tylko mignie i już go nie ma... ale powiedzmy, że w przypadku pluginów 3DMax-a dobrze będzie stosować starą dobrą metodę z systemów Unix - brak komunikatu jest dobrym komunikatem - czyli po prostu nic nam się niepożądanego nie stało.

Pora teraz na parametry metody DoExport() - oczywiście opis większości znajduje się w dokumentacji, ale tak pokrótce powiemy sobie co i jak. Pierwszy - to łańcuch znakowy - zawiera on nazwę pliku, którą wpiszemy w pokazanym powyżej oknie eksportu. Sam 3DMax weźmie tę nazwę i poprzez wewnętrzne mechanizmy przekaże ją metodzie DoExport() aby ta mogła korzystając z tego dobrodziejstwa posłużyć się tą nazwą i stworzyć odpowiedni plik do zapisu - plik ten oczywiście utworzyć, zapisać i zamknąć musimy sami, no ale właśnie tego mamy zamiar się tutaj dowiedzieć. Drugim ważnym dla nasz parametrem jest parametr drugi o groźnie wyglądającym typie ExpInterface* - co i jak już za moment, trzeci - także nie jest taki trywialny więc omówimy sobie go bliżej za chwileczkę. Dwa pozostałe to takie trochę "organizacyjne" i "upiększające" więc doczytajcie sobie o nich sami, my zajmiemy się sprawami większej wagi.

Weźmy pierwszy parametr - typu ExpInterface. Szperając w dokumentacji doczytamy, że to coś jest interfejsem klasy eksportu - brzmi nieźle, ale co to w ogóle robi? Otóż jest to klasa, z której tak naprawdę interesuje nas jeden jej składnik a mianowicie wskaźnik na kolejną klasę o nazwie IScene. Do używania i przedzierania się przez całe drzewa klas i interfejsów radzę się od razu tutaj przygotować, bo nieraz będziemy musieli jeszcze to robić a zapewniam, że programiści 3DMax-a nie szczędzili sobie na nazwach, dziedziczeniu i kombinowaniu - po prostu stworzyli potwora - no ale mam ambitny zamiar pomóc wam w jego ujarzmieniu i zapewniam, że na pewno nam się uda. Co robi klasa IScene natomiast to za chwilkę - wróćmy może jeszcze do trzeciego parametru metody DoExport(), czyli do typu Interface. To kolejna klasa a w zasadzie interfejs - tym razem już zaczyna naprawdę być interesująco, ponieważ jest to klasa, która daje nam dostęp do wszystkich metod 3DMax-a jako programu - możemy zatem do woli korzystać z wszystkich wyeksportowanych metod znajdujących się w plikach wykonywalnych 3DMax-a i możemy w nim zdrowo namieszać, jeśli tylko tego zechcemy :).
No ale ponieważ my nie chcemy mieszać a raczej robić bardziej pozytywne rzeczy, więc wróćmy do parametru pierwszego. Pamiętamy hierarchię? Dla przypomnienia - do metody DoExport() przekazany został nam parametr ExpInterface, który dał nam do ręki wskaźnik na obiekt typu IScene. Co zaś nam daje ten obiekt???

Otóż - Panie i Panowie, nadszedł czas aby powiedzieć sobie trochę o czymś takim jak scena w 3DMax. Popatrzymy na to najprościej jak się da, żeby było i szybko i bezboleśnie. Jak wiemy - na scenie 3DMax-a możemy umieścić bardzo dużo rzeczy - są to nie tylko obiekty w sensie siatek. Znajdzie się tam zapewne bardzo dużo świateł, kamer, różnorakich punktów kontrolnych, różnych systemów (ot choćby cząstek), tzw. helperów i co tylko nam się zamarzy. Nam programistom oczywiście nie ułatwi to życia, ale grafikowi pewnie tak. Czymś bardzo istotnym na scenie w 3DMax, patrząc od strony programistycznej jest nowa klasa, jak się okazuje bardzo ważna i niezastąpiona - klasa o nazwie INode. Nazwa sugeruje węzeł - i tak jest w istocie - klasa ta reprezentuje nic innego jak węzeł. Ale nie taki wiązany na chusteczce, tylko węzeł na scenie. Otóż wszystko, co znajduje się na scenie w 3DMax, a na pewno wszystko o czym powiedziałem powyżej jest reprezentowane wewnętrznie w programie jako węzły. Tak więc każda siatka, każda kamera, światło, helper czy cokolwiek będące namacalnym obiektem w 3DMax-ie jest dostępne nam programistom jako obiekt typu INode, z którego pomocą możemy się dobrać do takiego obiektu i wyssać z niego całą życiodajną, mniam... znaczy się uzyskać wszystkie pożyteczne i jakże przez nas pożądane dane. Oczywiście jest to klasa bazowa i trzeba będzie się mocno nagimnastykować, żeby dotrzeć do informacji właściwych, ale teraz to już wszystko da się zrobić ;). Dlaczego akurat węzły? Ano dlatego, żeby łatwo się było po nich poruszać. Wyobraźmy sobie, że ktoś wymyśliłby tak 3DMax-a, że każdy obiekt byłby nową klasą, bez ładu i składu - co groziłoby nam programistom? Musielibyśmy brać każdy obiekt na scenie, badać go czym jest, sprawdzać zapewne mnóstwo flag i wskaźników, szukać rodziców lub dzieci i strasznie byśmy się umordowali. A tak - mamy wszystkie obiekty w łańcuchu ładnie poukładane i możemy bardzo łatwo się takim łańcuchem posługiwać.

A do posługiwania się łańcuchem służy właśnie obiekt IScene - a dokładniej pomoże on nam wyliczać obiekty w łańcuchu, zaznaczać je i za pomocą odpowiednich mechanizmów dobierać się do nich w celach nam tylko wiadomych. Klasa IScene posiada jedną, jedyną metodę, która ułatwi całe nasze życie - nazywa się ona EnumTree(). Dlaczego znowu "tree" ktoś zapyta. Otóż scena w 3DMax-ie zbudowana jest tak, że nic nie fruwa w powietrzu - pomimo, że kamera jest na przykład swobodna a kulka spada sobie na ziemię, to każdy bez wyjątku obiekt jest czyimś dzieckiem a zdarza się, że i rodzicem. Pociąga to za sobą oczywiście pewnego rodzaju hierarchię, a jak powszechnie wiadomo tę najłatwiej jest przedstawić właśnie przy pomocy drzewa, w którym kolejne gałęzie są połączone węzłami, które są właśnie naszymi obiektami na scenie.

Metoda EnumTree() umożliwia więc nam przejście po drzewie od korzenia, którym jest scena (bo wszystkie obiekty należą do sceny) poprzez wszystkie węzły i sprawdzenie wszystkich obiektów pod różnymi względami, szczególnie tymi, które nas bardzo interesują. Jednak aby było to możliwe będzie nam potrzebna pomoc jeszcze jednego obiektu klasy ITreeEnumProc. Aby go jednak użyć, będziemy musieli stworzyć na jego bazie nową klasę i nadpisać w nim jedną metodę. Otóż metoda EnumTree() obiektu IScene pobiera jeden argument - wskaźnik na obiekt typu ITreeEnumProc właśnie, ponieważ wewnętrznie wywołuje z niego jego metodę o nazwie callback(), którą to właśnie musimy nadpisać. Natomiast metoda callback() obiektu ITreeEnumProc przyjmuje jako argument wskaźnik na klasę INode - dostaje więc węzeł sceny. Wiem, trochę to zakręcone, więc spróbujmy zebrać to do kupy.

Mamy obiekt sceny IScene, który ma metodę EnumTree(). Aby ona zadziałała musimy mu jednak podać jako argument wskaźnik na obiekt typu ITreeEnumProc, który z kolei posiada metodę callback(). Metoda ta zostanie wywołana wewnętrznie w metodzie EnumTree() obiektu sceny dla każdego węzła drzewa reprezentującego strukturę sceny. Tak więc jest to jakby takie sprzężenie - obiekt scena wywołuje obiekt nazwijmy go wyliczania drzewa i wewnątrz tego wywołania przekazuje mu kolejne węzły drzewa, które sam kontroluje - ech, mam nadzieję, że rozumiecie ;). Nie koniec na tym - aby to wszystko zadziałało musimy zrobić jeszcze lepszy motyw - tutaj proszę się skupić, bo będzie szczególnie trudno ;). Otóż, żeby to wszystko ruszyło jako tako my w programie zrobimy sobie tak:
  • Podziedziczymy klasę ITreeEnumProc,
  • Nadpiszemy jej metodę callback(),
  • Jako parametry konstruktora tejże przekażemy obiekt klasy IScene,
  • W konstruktorze klasy dziedziczonej z ITreeEnumProc zawołamy metodę sceny EnumTree() przekazując jej jako parametr wskaźnik na obiekt, w którym się właśnie znajdujemy ;).
He, he... mam nadzieję, że nadążacie, ale wyobrażam sobie jak klniecie na to wszystko - już bardziej pokręcić tego nie było można ktoś pewnie zapyta. Powiem szczerze, że ja sam wzorowałem się na przykładowych pluginach 3DMax i tam właśnie z powodzeniem stosują podobną taktykę - założyłem, więc że jest dobra a jak czas pokaże - sprawdzi się doskonale w naszym przypadku ;).

Wejdźmy więc wreszcie do środka naszej metody DoExport(), o której pewnie większość już zdążyła zapomnieć i nie wie do czego ona miała służyć ;). Może wreszcie zobaczmy kawałek jakiegoś ludzkiego kodu:
int Export::DoExport( const TCHAR *name,ExpInterface *ei,Interface *gi, BOOL suppressPrompts, DWORD options )
{
  CSceneEnumProc myScene( ei->theScene, name );

  return TRUE;
}
Funkcja jak widać niej jest zbyt skomplikowana - w zasadzie to jest tam tylko moment tworzenia obiektu typu CSeneEnumProc. Klasa ta jest wspomnianą przeze mnie powyżej klasą dziedziczącą po obiekcie typu ITreeEnumProc, który pomoże nam się dobrać do poszczególnych węzłów drzewa sceny za pomocą obiektu IScene. Klasę tę zgodnie z tym co powiedzieliśmy sobie wcześniej wyposażyliśmy w nadpisaną metodę callback() oraz w nowy konstruktor, który przyjmuje trochę różnych parametrów - tylko te dwie metody będą nam potrzebne do wyliczenia sobie elementów sceny i dobrania się do poszczególnych danych. I po kolei powiemy sobie teraz co to za parametry konstruktora obiektu typu CSeneEnumProc:

Jako pierwszy przekazujemy do konstruktora obiekt typu IScene. Za pomocą tego interfejsu w konstruktorze wywołamy metodę EnumTree() przekazując jej jako parametr wskaźnik na obiekt, w którym metoda ta zostanie wywołana, czyli w skrócie mówiąc zrobimy tak:
CSceneEnumProc::CSceneEnumProc( IScene* scene, const char* szName )
{
  ...
  scene->EnumTree( this );
  ...
}
W ten spsób właśnie przekażemy obiektowi sceny wymagany przez niego obiekt typu ITreeEnumProc a w zasadzie obiekt klasy dziedziczącej po nim, no ale dzięki dziedziczeniu i wszystkim innym zaletą języka C++ nie będzie to dla niego wielką przeszkodą.
Powracając jeszcze do naszych argumentów - drugi jest chyba zupełnie oczywisty. Jest to nazwa pliku, który przyjdzie z okienka exportu, poprzez metodę DoExport() obiektu klasy Export (dziedziczącej z SceneExport) i posłuży nam do stworzenia i obróbki pliku, w którym dane zostaną zapisane.
Obiekt klasy CSceneEnumProc więc stworzymy a to jak każdy, kto zna choć najmniejsze podstawy C++ wie, powoduje wywołanie adekwatnego konstruktora z klasy. Czas więc aby zobaczyć co takiego w naszym konstruktorze się dzieje:
CSceneEnumProc::CSceneEnumProc( IScene* scene, const char* szName )
{
  file.open( szName, ios::out );
  {
    scene->EnumTree( this );
  }
  file.close();
}
No i cóż - chyba wszystko jasne. Tworzymy obiekt (strumień), który będzie odpowiedzialny za operacje na pliku - powinien to być obiekt globalny albo member klasy aby inne metody lub obiekty miały do niego dostęp, następnie posługując się nazwą dostarczoną jako parametr funkcji otwieramy go do zapisu, wywołujemy funkcję enumeracji sceny i zamykamy. Powstaje pytanie - co z danymi - gdzież one do diabła są??? Otóż - powiedzieliśmy sobie przecież o mechanizmie działania funkcji enumeracji z obiektu sceny. Leci ona po wszystkich węzłach sceny po kolei i przy każdym woła metodę obiektu typu ITreeEnumProc::callback() przekazując mu do środka wskaźnik na kolejne węzły. Ponieważ u nas obiektem, który będziemy przekazywać jest ten, w którym się właśnie znajdujemy, więc logiczne jest, że w tym właśnie momencie zacznie się seria wywołań jego metody callback(), którą musimy nadpisać aby to wszystko działało. Napiszmy więc naszą metodę:
int CSceneEnumProc::callback( INode* inode )
{
  return 0;
}
Widać, co metoda przyjmuje - węzeł, czyli to, co w danym momencie nas najbardziej interesuje. Wewnątrz metody IScene::EnumTree() metoda ITreeEnumProc::callback() zostanie zawołana tyle razy, ile węzłów jest na scenie - dla każdego po kolei. A stąd już naprawdę niedaleko do geometrii - jeszcze troszeczkę cierpliwości ;).

Teraz czas powiedzieć sobie o obiektach w 3DMax - ale nie o tych znajdujących się na scenie ale o wszystkich dostępnych. Powiedziałem na początku o węzłach - te są potrzebne do poruszania się po scenie głównie, aby był jakiś porządek i żeby można było mieć nad tym jakąś kontrolę. No ale wiadomo, że nie z samej sceny się składa 3DMax. Gdzieś u podstaw na pewno leży cała góra innych obiektów, które zajmują się zarządzaniem, organizacją i stanowią solidną podstawę dla całej reszty. Tak naprawdę, to co widzimy na ekranie to tylko wierzchołek góry lodowej, jeśli mogę wprowadzić takie porównanie ;).
Jednym z podstawowych obiektów budulcowych 3DMax-a są obiekty bardzo pożytecznej klasy o nazwie Object. Klasa ta posiada bardzo wiele ważnych zadań - jeśli przeczytać SDK to można się dowiedzieć że jest ona odpowiedzialna między innymi za geometrię obiektu, mapowanie tekstur, jego przekształcanie, określanie różnych stanów, ale także stanowi podstawę dla obiektów, które nie są geometrią - jak na przykład znane grafikom 3D modyfikatory. Jednym słowem klasa ta jest tak samo ważna co i pożyteczna i my w naszych rozważaniach na pewno się do niej odwołamy. Zaczniemy już na samym początku. Ponieważ obiekt na scenie, będący węzłem na pewno jest także obiektem, więc musimy się dobrać do tegoż w jakiś sposób, ponieważ będziemy potrzebować kilku jego metod do dalszego działania a bez nich po prostu nie damy rady. Aby dotrzeć do obiektu klasy Object wystarczy zawołać odpowiednią metodę z klasy INode:
Object* pObject = inode->GetObjectRef();
Po tym fragmencie, jeśli wszystko jest w porządku na scenie, w ręce będziemy mieć wskaźnik na obiekt typu Object skojarzony z danym węzłem. Po co zaś nam ten wskaźnik - oczywiście po to, aby wołać różne pożyteczne metody tego obiektu.

I znowu musimy się trochę oderwać od kodu źródłowego aby sobie wspomnieć o pewnej rzeczy - są nią tzw. Klasy i SuperKlasy obiektów. To, że w 3DMaxie na scenie może być prawie wszystko to wiemy doskonale. To za sobą oczywiście pociąga potrzebę kontroli nad tym wszystkim, tak aby w najprostszej sytuacji, kiedy np. klikniemy myszką w coś na ekranie 3DMax był w stanie dowiedzieć się, co to właściwie było. Do tego celu służą liczbowe identyfikatory - wspomniane właśnie przeze mnie powyżej Klasy i SuperKlasy. Mają one za zadanie jednoznacznie określić i zakwalifikować dany obiekt do danej grupy. Taki identyfikator składa się w zależności od typu klasy albo z jednej albo z pary liczb, które jednoznacznie i w sposób unikalny określają typ obiektu z jakim mamy do czynienia. Klasy i SuperKlasy różnią się od siebie tym, że istnieją na trochę innym poziomie - SuperKlasy określają większe skupiska obiektów (i są to pojedyncze liczby), które łączy jakaś cecha wspólna - na przykład SuperKlasą będzie klasa określająca obiekty geometryczne - z tej zresztą będziemy mieli okazję za moment skorzystać. Zwykłymi klasami będą natomiast podklasy tejże SuperKlasy - np. obiekty zbudowane z trójkątów, z tzw. łatek (ang. Patches), będą to klasy wszystkich prymitywów dostępnych w 3Dmaxie na starcie (box, torus, sphere itd...) oraz kilka innych - klasa składa się z dwóch liczb. Obiekty klasy Object oraz klasy czy też identyfikatory obiektów niewątpliwie coś łączy - coś co bardzo nas powinno interesować. Odpowiedź oczywiście nasuwa się sama - za pomocą klasy Object będziemy mogli sobie sprawdzić jaką Klasę i SuperKlasę posiada dany obiekt na scenie. Oczywiste jest, że podczas naszej wędrówki po drzewie sceny nie każdy obiekt będzie nas interesował w równym stopniu, więc trzeba będzie się jakoś dowiedzieć co jest czym. Do tego celu wykorzystamy dwie metody obiektu klasy Object:
Class_ID  classID = pObject->ClassID();
SClass_ID superClassID = pObject->SuperClassID();
Metody które przedstawiłem powyżej zwrócą nam odpowiednio Klasę i SuperKlasę danego obiektu, który pozyskaliśmy z kolei z węzła INode gdzieś wcześniej. Teraz sprawdzając najpierw SuperKlasę a potem Klasę będziemy mogli skutecznie dowiedzieć się co jest obiektem nas interesującym a co nie.
Ponieważ my poszukujemy geometrii więc interesowała nas będzie przede wszystkim SuperKlasa o identyfikatorze GEOMOBJECT_CLASS_ID. Wspomniałem wyżej, że Klasy jako identyfikatory to liczba lub dwie. Klasy (SuperKlasy nie!) - to tak naprawdę obiekty z języka C++ - a te posiadają pewne metody i operatory potrafiące odpowiednio przekształcić dane w obiekcie na postać dla nas wygodniejszą w użyciu. Tak więc w plikach nagłówkowych istnieje mnóstwo instrukcji #define, które identyfikują parę liczb z takim identyfikatorem, jaki przedstawiłem dla geometrii - dla SuperKlasy jest oczywiście podobnie, z tym, że w tym przypadku jest to jedna liczba. Polecam także przy okazji przeglądanie plików źródłowych z SDK, bo można się tam wiele dowiedzieć i jak czegoś nie ma w dokumentacji, to na pewno jest tam ;).
Wracając zaś do tematu - czy porównamy ze sobą liczbę, dwie liczby czy taki identyfikator jak powyżej to wszystko jedno. Oczywiście wygodniej się będzie posługiwać prostym identyfikatorem niż dwoma liczbami, więc od tej pory zapamiętajmy sobie ten, dla nas jedne z najważniejszych...

Załóżmy więc, że nasz program działa, na scenie jest całe stado różnych obiektów i my jesteśmy w naszej metodzie IScene::EnumTree(). Wołana jest zatem metoda ITreeEnumProc::callback() dla każdego węzła. W tej metodzie więc musimy sprawdzić z jakim typem obiektu mamy do czynienia.
switch( superClassID )
{
  case GEOMOBJECT_CLASS_ID:
      {
      }
      break;

  default:
      break;
}
Prostą instrukcją switch-case sprawdzimy sobie SuperKlasę nadchodzącego właśnie węzła naszej sceny. Jeśli będzie to obiekt reprezentujący geometrię to odpowiednio na to zareagujemy - postaramy się wyciągnąć z niego dane o geometrii. Jeśli będzie to coś innego, to pominiemy na razie, ponieważ te nas na razie nie interesują. Jest to taki pierwszy etap selekcji obiektów na scenie, ponieważ tak naprawdę do prawdziwej geometrii jeszcze kawaleczek drogi. Następnym elementem wymagającym sprawdzenia będzie to, czy obiekt jest złożony z trójkątów (bo takie raczej będą nas interesować) czy też nie. Oczywiście jeśli komuś potrzeba innego rodzaju obiekty to sobie odpowiednio to zaimplementuje, ale my podążymy śladem podstaw na razie.
SuperKlasa GEOMOBJECT_CLASS_ID posiada według posiadanej przeze mnie dokumentacji następujące podklasy:

Wbudowane w jądro programu:
  • TRIOBJ_CLASS_ID
  • PATCHOBJ_CLASS_ID
Prymitywy:
  • BOXOBJ_CLASS_ID
  • SPHERE_CLASS_ID
  • CYLINDER_CLASS_ID
  • CONE_CLASS_ID
  • TORUS_CLASS_ID
  • TUBE_CLASS_ID
  • HEDRA_CLASS_ID
  • TEAPOT_CLASS_ID1
  • TEAPOT_CLASS_ID2
  • PATCHGRID_CLASS_ID
Particle:
  • RAIN_CLASS_ID
  • SNOW_CLASS_ID
Większość z tych klas niewiele nas na razie będzie obchodzić, ale jest jedna, szczególnie dla nas ważna a mianowicie TRIOBJ_CLASS_ID. Wystarczy sprawdzić czy nadchodzący obiekt posiada SuperKlasę GEOMOBJECT_CLASS_ID oraz klasę TRIOBJ_CLASS_ID i już jesteśmy prawie w domu - będziemy wiedzieć na pewno, że dany węzeł zawiera nasz upragniony obiekt złożony z trójkątów, gotowy do wyeksportowania. Sprawdźmy więc czy nasz obiekt ma odpowiednią klasę, bo SuperKlasę już sprawdziliśmy powyżej:
case GEOMOBJECT_CLASS_ID:
  {
    if( 0 != pObject->CanConvertToType( Class_ID( TRIOBJ_CLASS_ID, 0 ) ) )
    {
    }
    else
    {
      return 0;
    }
  }
  break;
Weźmy kawałek naszego "switcha". Niestety, tak się składa, że Klasa to już dwie liczby a nie jedna, więc nie możemy sobie sprawdzić, czy obiekt jest danej klasy po prostu za pomocą prostej instrukcji porównania czy switch-case. Zamiast tego wykorzystamy metodę klasy Object pod nazwą CanConvertToType(). Metoda ta potrafi sprawdzić czy dany obiekt będzie można w jakiś tam wewnętrzny sposób 3DMax-a przekształcić do obiektu danej klasy. Jako parametr metoda ta przyjmuje obiekt klasy Class_ID. Aby stworzyć taki obiekt posłużymy się oczywiście konstruktorem klasy Class_ID, który jako parametry przyjmować może różne rzeczy - tutaj są akurat dwie liczby definiujące identyfikator liczbowy określający obiekt złożony z trójkątów.
Metoda CanConvertToType() sprawdza więc, czy możemy dany obiekt skonwertować do obiektu złożonego z trójkątów, jeśli jest to możliwe funkcja zwróci nam wartość różną od zera , jeśli nie to będzie to zero i opuścimy naszą funkcję tak samo szybko, jak do niej weszliśmy.
if( 0 != pObject->CanConvertToType( Class_ID( TRIOBJ_CLASS_ID, 0 ) ) )
{
  pTriObject = (TriObject*) pObject->ConvertToType( 0, Class_ID( TRIOBJ_CLASS_ID, 0 ) );
}
No a skoro skonwertować będziemy sobie mogli, więc nie pozostanie nam nic innego, niż właśnie to zrobić!
Obiekt złożony z trójkątów będzie reprezentować klasa o nazwie TriObject. Jak mówi dokumentacja jest to możliwy do wyrenderowania, deformowalny obiekt siatki złożony z trójkątów, czyli dokładnie to, o co nam chodzi! Aby więc zbytnio nie przedłużać wykorzystujemy metodę obiektu Object o nazwie ConvertToType(). Przyjmuje ona dwa argumenty. Może tym razem zaczniemy od drugiego, który jest dokładnie taki sam jak w przypadku metody CanConvertToType(). Natomiast pierwszy oznacza chwilę czasu, w jakiej dokonujemy konwersji. Jak doskonale powinniśmy zdawać sobie sprawę 3DMax jest programem używanym głównie do modelowania i animacji - a to czym jest każdy wie. Nieuniknione więc jest to, że w którymś momencie musimy w końcu natrafić na ten brakujący element w tej całej zabawie czyli czas. Obiekt złożony z trójkątów jak już powiedzieliśmy sobie może się deformować - więc w różnych momentach czasu może mieć różne kształty i tylko od nas zależy w którym momencie my wyeksportujemy sobie jego geometrię. Załóżmy jednak dla uproszczenia, że my będziemy tylko modelować, na razie bez ruchu naszych bohaterów. Czas więc w tym przypadku nie ma aż tak dużego znaczenia, więc przyjmijmy, że nasz eksport odbędzie w momencie startu animacji, czyli po prostu 0. Dla prostej potrzeby eksportu geometrii jest to zupełnie wystarczające.
pTriObject = (TriObject*)pObject->ConvertToType( 0, Class_ID( TRIOBJ_CLASS_ID,0 ) );
pMesh = &( pTriObject->GetMesh() );
No i w tym momencie można powiedzieć, że już całkiem jesteśmy w domu. Mamy w ręce wskaźnik do obiektu klasy TriObject, który daje z kolei mnóstwo metod umożliwiających bezpośrednio pracę nad konkretną siatką czy też modelem - zależy jak to nazwać. Już pierwsze pole z opisu dokumentacji daje odpowiedź na dręczące nas od dawna pytania - klasa TriObject posiada bowiem w sobie obiekt o jakże pięknie brzmiącej nazwie - czyli po prostu Mesh. A jeśli zaglądnąć do dokumentacji pod hasło class Mesh to oczom naszym ukazuje się to, co tygrysy lubią najbardziej - całe mnóstwo pól i funkcji, które są czystą esencją tego, co potrzebujemy ;).
Wykorzystując odpowiednie pola dobierzemy się i do danych wierzchołków tworzących siatkę (Mesh), do indeksów tych wierzchołków, do współrzędnych mapowania tekstur i wszystkiego co nam jest potrzebne. Opisywać można by naprawdę długo - jest tego całe mnóstwo a stopień skomplikowana rośnie z każdą omówioną rzeczą. Zwrócę może wam uwagę tylko na kilka rzeczy, ponieważ są one ważne i możecie się nieźle naciąć na tych dziwactwach, ale prostych nie będę omawiał, bo nawet po pobieżnym przejrzeniu dokumentacji każdy zrozumie o co w tym chodzi.
  • Po pierwsze - nie zdziwcie się, że ilość tzw. teksturowanych wierzchołków będzie się różnić w większości przypadków od ilości wierzchołków w siatce. Wielu z was zna doskonale ten problem - wyobraźcie sobie na przykład bryłę złożoną z dwóch trójkątów, z których każdy ma inną teksturę nałożoną na siebie. Wiemy doskonale czym są współrzędne mapowania tekstur. W naszym przypadku te dwa trójkąty stykają się ze sobą dwoma wierzchołkami. Cała bryła ma więc cztery wierzchołki, z czego dwa wspólne dla obydwu trójkątów. Istnieje taki przypadek, który da się uogólnić na wszystkie pozostałe - otóż te dwa wspólne wierzchołki posiadają różne współrzędne mapowania dla trójkąta jednego i drugiego. Jeśli wyeksportujemy naszą bryłę z 3DMax-a nie zwracając uwagi na takie rzeczy to wyjdzie nam kaszana, bo w żaden sposób nie jesteśmy w stanie wyrenderować takiej bryły za pomocą Direct3D czy OpenGL, korzystając na przykład z indeksowanych wierzchołków. Indeks dla obydwu trójkątów wskażą nam te same wierzchołki, które jednak posiadać będą tylko jeden zestaw współrzędnych mapowania- dla któregoś trójkąta więc zostaną one zgubione i mapowanie będzie zupełnie do bani. Jedynym wyjściem z tej sytuacji jest duplikacja wierzchołków - tych, które należą do kilku ścian a jednocześnie posiadają dla każdej inne współrzędne mapowania. Za pomocą dostępu do wierzchołków teksturowanych w siatce nie powinniście mieć problemu z opracowaniem odpowiedniego algorytmu powtarzania i dobierania współrzędnych.
  • Drugą ważną sprawą jest macierz przekształcenia. Wielu może się zdziwić w momencie, kiedy uda im się już cokolwiek wyeksportować, że obiekty nie są położone tak jak sobie to wyobrażali, są poprzekręcane i ułożone w zupełnie odmienny sposób niż na scenie w 3DMax. Dzieje się tak, ponieważ 3DMax domyślnie przy eksporcie umieści nasz obiekt w centrum swojego świata i w ten sposób przechowuje dane jego geometrii - wszystko względem środka. To, że na scenie max-owej obiekt jest gdzieś przesunięty zawdzięczamy jego macierzy przekształcenia. Jeśli eksportujemy kilka obiektów powiedzmy jako całość to jeśli nie uwzględnimy ich macierzy przekształcenia na scenie to wszystkie nałożą nam się na środku układu współrzędnych. Aby tego uniknąć wystarczy pobrać wierzchołki obiektu i przemnożyć je przez macierz przekształcenia, którą możemy dla danego węzła pobrać za pomocą metody klas INode - GetObjectTM(), która jako argument pobiera czas, ponieważ obiekt oczywiście może być animowany w czasie. Ale jeśli zależy nam tylko na geometrii to wystarczy wstawić początek, czyli zero.
  • Trzecią podstawową wpadką może być nie uwzględnienie stosu modyfikacji. Wspominałem na samym początku nauki, że 3DMax jest na tyle wredny, że przechowuje całą geometrię w oryginale i wszystkie parametry modyfikatorów na stosie modyfikacji - dlatego zewnętrzne przeglądarki plików 3D nie potrafią plików takich zinterpretować poprawnie. Jeśli teraz zastosujemy do naszego obiektu szereg modyfikatorów i pozostawimy stos niescalony (polecenie collapse stack) (grafik powinien wiedzieć o co chodzi) to pomimo, że do naszego obiektu zastosujemy na przykład dziesięć różnych modyfikacji to przy eksporcie zostanie nam pobrana podstawowa geometria, z której my tworzyliśmy naszą ostateczną formę obiektu. Wyjścia mamy dwa - albo dobrać się do stosu modyfikacji, wtedy pobieramy kolejne modyfikatory i wyciągając z nich różne parametry modyfikujemy nasze wierzchołki, no ale omówienie tego wykracza ogromnie poza ramy tego artykułu. Albo pozostaje nam scalenie stosu modyfikacji. Sposób powiedziałbym dosyć barbarzyński ale niezwykle prosty i skuteczny. Tylko tutaj uwaga! Po scaleniu stosu modyfikacji stracimy historię zmian, więc lepiej robić to na jakieś kopii.
  • Czwarta wpadka - grupy. Jeśli eksportujemy na przykład obiekt, który jest zgrupowany razem z innymi. Trzeba tutaj oczywiście zbadać taki przypadek i albo odpowiednio rozbroić grupę albo jakoś inaczej to potraktować. Oczywiście wszystko zależy od konstrukcji i naszych potrzeb - czy obiekt składać ma się z elementów należących do jednego mesha, ale mogących od niego na przykładać odpadać. Dobrym przykładem może być na przykład samochód, który ma odpadające koła, lusterka, zderzaki itd..., czy to będzie cały mesh, nierozerwalny. Tutaj opcji jest oczywiście bardzo wiele i trudno wybrać jakąś właściwą. Wszystko zależy od tego, jak sobie to zaplanujecie i jakie macie zapotrzebowanie na komplikację obsługi waszych brył.
No i to w zasadzie wszystko jeśli chodzi o podstawy eksportu. Oczywiście nie przedstawiłem tutaj wszystkich aspektów, bo to jest po prostu niemożliwe - każdy ma inne potrzeby i możliwości i każdy inaczej będzie sobie organizował ten eksport. Drogę do geometrii i pułapki czyhające po drodze znacie - to powinno wam oszczędzić sporo pracy, którą niestety sam musiałem wykonać, aby móc się podzielić z wami. Zapewniam jednak, że to dopiero początek tej fascynującej przygody z 3DMax-em a wgłębiając się w dokumentację i przykłady można całe życie spędzić tylko na pisaniu pluginów ;).
Mam nadzieję, że teraz już nie będziecie mieć wymówek, że brak wam geometrii do waszych aplikacji - po napisaniu prostego eksportera z 3DMax-a i importera do aplikacji już nic nie ograniczy waszej twórczej pasji - czekam na jakieś wiekopomne dzieła ;). Do następnego zobaczenia więc...

©Copyright by Robal   



Tutoriale - Techniki
Nasze newsy s� w RSS: backend.php
PHP-Nuke Copyright © 2005 by Francisco Burzi. This is free software, and you may redistribute it under the GPL. PHP-Nuke comes with absolutely no warranty, for details, see the license.
Tworzenie strony: 0.05 sekund

:: Layout strony został stworzony przez www.nukemods.com w oparciu o styl phpbb2 Helius, którego autorem jest Cyberalien ::