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 38 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ę?ć trzecia

Witam. Dzisiaj szaleństwa eksportowego ciąg dalszy, czyli omówię nieco dokładniej jak powyciągać resztę jakże przydatnych rzeczy z Max'a i jak dobrać się do poszczególnych danych geometrii. Jednakże pierwszą rzeczą jaka chciałem zrobić jest podziękowanie Robalowi za dwie poprzednie części. Bez nich nie napisałbym tego tutka, a także nie powstałby eksporter i format jakże niezbędny do dalszej pracy w dziale OpenGL. Tak wiec dzięki Robal :). Wydaje mi się, że to już będzie ostatni tutek z tej serii (takiej prawie teorii), a na pewno ostatni traktujący o geometrii statycznej.

Ostatnio skończyliśmy posiadając obiekt złożony z trójkątów (TriObject), a właściwie jego siatkę (Mesh). Teoretycznie mamy już wszystko czego chcieliśmy, bo wydawałoby się, że teraz tylko wyciągniemy współrzędne wierzchołków, normalne, współrzędne dla tekstur i będzie git. Po kilku prostych próbach może się jednak okazać, że to wcale nie jest takie znowu proste...

Zacznijmy od trójkątów. Obiekty w Max'ie są zbudowane w ten sposób, że w jednej tablicy siatki znajdują się wszystkie wierzchołki, a w drugiej wszystkie trójkąty zapisane jako indeksy do tablicy pierwszej. Np. tablica pierwsza zawiera:
Vertex0 (0.0f, 1.0f, 0.0f)
Vertex1 (-1.0f, 0.0f, 0.0f)
Vertex2 (1.0f, 0.0f, 0.0f)
Wtedy, aby otrzymać trójkąt w tablicy trójkątów (będę je nazywał z angielska face) będzie jeden element : Face0 = 0, 1, 2. Gdzie 0, 1, 2 to indexy do pierwszej tablicy. Przy czym te indeksy są zapisane w tablicy faces o nazwie v. Czyli pierwszy index to faces.v[0]. W ten sposób powstanie nam trójkąt. Pierwsza tablica jest to składowa siatki o nazwie verts, natomiast druga jak nie trudno się domyślić to składowa faces. Ilość wierzchołków można pobrać poprzez metodę siatki getNumVerts(), natomiast ilość face'ów poprzez metodę getNumFaces(). Pozostało nam już tylko wyciągnąć macierz przekształcenia i albo zapisać ją sobie w pliku do późniejszego użytku, albo wymnożyć każdy wierzchołek (i normalną!!) po kolei podczas eksportu. Macierz przekształcenia można pobrać poprzez metodę klasy INode o nazwie GetObjectTM(czas). Metoda ta zwraca klasę Matrix3 (teraz wystarczy już tylko np. wymnożyć sobie wierzchołki przez tą macierz, jako, że w Max'ie są zdefiniowane odpowiednie operatory). Jednakże warto pamiętać, że ta macierz jest nieco inna niż standardowa, ponieważ wewnętrznie Max posługuje się kwaternionami, a nie macierzami. Jeśli chcecie się dowiedzieć czegoś więcej o przekształceniach w Max'ie, to oczywiście odsyłam do SDK. Teoretycznie mając te dane mamy już gotowy obiekt i teraz wystarczy już tylko wrzucić to do pliku. I rzeczywiście jest to prawda, ale tylko dopóki nie chcemy włączyć oświetlenia. Ponieważ nie wyciągnęliśmy jeszcze normalnych, więc załóżmy na chwilkę, że przypisaliśmy je wierzchołkom ręcznie - powiedzmy, że obiekt jest prostą kostką. Jeżeli wyrenderujemy teraz naszą kostkę przy włączonym oświetleniu, to się okaże że jest ona jakoś dziwnie oświetlona. Dzieje się tak dlatego, że w kostce znajdują się wierzchołki, które powinny posiadać więcej niż jedna normalną - są to wierzchołki narożne. Max rozwiązuje problem w ten sposób, że definiuje coś co nazywa się grupami cieniowania (smoothing groups). Każdy face w Max'ie ma przypisany numer grupy do jakiej należy. Weźmy dalej naszą kostkę. Jak nietrudno się domyślić ma ona sześć grup cieniowania - po jednej dla każdej ściany. A ile takich grup może mieć kula? Ano oczywiście, że jedną. Ogólnie rzecz biorąc w grupie znajdują się wszystkie te trójkąty, dla których mogą być uśredniane normalne. Jeżeli w siatce znajdują się ostre krawędzie, to jest pewne, że siatka zawiera grupy cieniowania. Niestety programiści piszący Max'a chyba bardzo lubią motać, bo pobranie numeru grupy też nie polega tylko na pobraniu jakiejś składowej. Robi się to w ten sposób:
for (int i=0; i<32; i++)
{
  if (pMesh->faces[FaceIndex].smGroup & (1<<i))
  {
    return (i+1);
  };
};
Jak widać z powyższego kodu smGroup to 32 bitowa składowa siatki, a poprawny numer grupy uzyskujemy poprzez stworzenie maski (1<<i) i porównanie jej (bitowo) z naszą składową. Ponieważ chcemy mieć grupy od 1 do 32 toteż dodajemy 1 do i aby to uzyskać :). Teraz, kiedy już wiemy co to są grupy możemy powielić te wierzchołki, które w różnych trójkątach należą do różnych grup. A mając takie dane możemy spokojnie przystąpić do wyciągnięcia normalnych z Max'a.

Pisząc eksporter pomału zapoznawałem się z Max'em i niestety, ale muszę stwierdzić, że jego twórcy chyba celowo chcieli ludziom utrudnić życie... Kod, który wyciąga normalne dla wierzchołków poszczególnych trójkątów dobitnie na to wskazuje :). Tak więc normalne dla wierzchołków zapisane są w osobnej tablicy. Niestety ta tablica de facto nie zawiera normalnych, ale wierzchołki :). Jest to tablica typu RVertex, który to typ jest w Max'ie tłumaczony jako Render Vertex ;). Dla nas jednak najważniejsze jest, że przechowuje ona również normalne dla danego wierzchołka. Trik polega na tym, że jeżeli wierzchołek należy do różnych grup, to ten pojedynczy wierzchołek zawiera wiele normalnych, choć oczywiście zdarzają się sytuacje kiedy mimo posiadania jednej grupy wierzchołek posiada wiele normalnych - takimi sytuacjami jednak nie będziemy się zajmować. Jeżeli jest jedna normalna, to jest ona zapisana w zmiennej rn klasy RVertex. Jeśli jest więcej normalnych, to są one w tablicy ern. Wskaźnik na tablicę RVertex możemy uzyskać poprzez metodę siatki getRVertPtr(index), gdzie index jest indexem wierzchołka (dla którego szukamy normalnej) w tablicy wierzchołków. Ten kod pochodzi z mojego eksportera, który zostanie omówiony w najbliższym czasie (poza funkcjami, które omówię tutaj ;))
Point3 GetNormal(Mesh *pMesh, int FaceIndex, RVertex *pRVert)
{
  int    NormalsNum = 0;
  Point3 Normal;

  if (pRVert->rFlags & SPECIFIED_NORMAL)
  {
    Normal = pRVert->rn.getNormal();
  }
  else if ((NormalsNum = pRVert->rFlags & NORCT_MASK) && pMesh->faces[FaceIndex].smGroup)
  {
    if (NormalsNum==1)
    {
      Normal = pRVert->rn.getNormal();
    }
    else
    {
      for (int i=0; i<NormalsNum; i++)
      {
        if (pRVert->ern[i].getSmGroup() & pMesh->faces[FaceIndex].smGroup)
        {
          Normal = pRVert->ern[i].getNormal();
        }
      };
    };
  }
  else
  {
    Normal = pMesh->getFaceNormal(FaceIndex);
  };

  return Normal;
};
I pomyśleć, że to służy tylko do wyciagnięcia normalnej :). Prześledźmy tą funkcję, aby zobaczyć dokładnie co też się tam dzieje.
  if (pRVert->rFlags & SPECIFIED_NORMAL)
  {
    Normal = pRVert->rn.getNormal();
  }
Najpierw sprawdzany czy normalna jest "specyfikowana". Co też to znaczy? Otóż powiem szczerze, że nie mam pojęcia ;). Ciekawe jest to, że w zasadzie sami twórcy Max'a nie mają pojęcia ;)). Znalazłem to w SDK do Max'a i w zasadzie jedynym opisem jaki to miało był tekst, że się tego teraz (mam wersje 4.2 Max'a) nie używa, ale należy dopisać taki fragment, bo może być używany w przyszłości. No to proszę bardzo - dopisałem :P.
else if ((NormalsNum = pRVert->rFlags & NORCT_MASK) && pMesh->faces[FaceIndex].smGroup)
Jak widać flagi w klasie RVertex spełniają dość ważną rolę. Dzięki masce NORCT_MASK możemy się dowiedzieć ile normalnych ma dany wierzchołek. W warunku sprawdzamy czy są w ogóle normalne (dla wierzchołka) i czy dany trójkąt znajduje się w jakiejś grupie cieniowania. Jeśli nie, to normalna dla wszystkich wierzchołków trójkąta jest na pewno taka sama i jest równa normalnej dla tegoż trójkąta. Jeśli natomiast mamy jakieś normalne dla tego wierzchołka, to już wiadomo - jeśli jest to pojedyncza normalna, to jest w składowej rn. Jeśli nie, to musimy znaleźć odpowiednią normalną (dla naszego trójkąta, który jest w jakiejś konkretnej grupie cieniowania) w tablicy ern. I to koniec. Po przeprowadzeniu wszystkich tych sprawdzeń mamy już w rękach normalne dla naszego obiektu. Tylko należy pamiętać, aby wcześniej (przed rozpoczęciem eksportowania danego obiektu) wywołać metodę siatki o nazwie buildNormals(), bo inaczej to dostaniemy NULL i wyskoczy nam błąd (albo nie, jeśli obsługujemy taki przypadek) i nie dostaniemy żadnej normalnej.

W zasadzie mając geometrię obiektu i jego normalne zobaczymy już poprawne wyniki po wczytaniu tego wszystkiego do naszego progsa. Jednakże w dalszym ciągu czujemy jakiś niedosyt. Otóż nie mamy przecież jeszcze tekstury, a bez tego ani rusz. Tekstury jak wiadomo w Max'ie przypisuje się do obiektu poprzez materiały. Tak więc najpierw musimy się dobrać do materiału, a dopiero później do tekstur. W sumie dobranie się do materiału jest banalnie proste. Wystarczy wywołać INode->GetMtl() i dostaniemy wskaźnik na materiał używany przez obiekt. Jednakże jak zwykle jest jakieś "ale". Otóż każdy, kto miał styczność z Max'em wie, że ma on naprawdę dużo różnych materiałów, a każdy z nich cechuje się innymi parametrami. Ponadto część z tychże materiałów jest zrobiona jako plug-iny i raczej ciężko się do nich dobrać. Dlatego też, tak jak przy obiekcie, należy sprawdzać jakiego typu jest materiał i jeśli się da, to rzutować na tym, który jest zdefiniowany wewnętrznie w Max'ie. Takich materiałów jest trochę, ale ja wymienię dwa, które są chyba najczęściej używane. Ponadto są to takie materiały, których możemy użyć w naszych programach (z resztą może być lekki problem, jako że ciężko je odwzorować). Pierwszy to oczywiście Standard Material, a drugi to Multi/Sub-Object Material. W zasadzie drugi to po prostu zbiór materiałów pierwszego typu :). Jak sprawdzić jakiego typu jest materiał? W taki sposób jak i obiekt, czyli poprzez jego klasę (class_ID). Piszemy więc
if (pMtl->ClassID() == Class_ID(DMTL_CLASS_ID, 0))
DMTL_CLASS_ID to identyfikator standardowego materiału. Natomiast Multi/Sub-object material ma identyfikator MULTI_CLASS_ID. Po takim sprawdzeniu możemy sobie spokojnie zrzutować z klasy Mtl na klasę StdMat, albo MultiMtl. Oczywiście aby pobrać właściwości materiału nie musimy go rzutować. Kolory, przezroczystość itd. można pobrać z klasy Mtl. Służą do tego funkcje:
  • GetAmbient()
  • GetDiffuse()
  • GetSpecular()
  • GetShininess()
  • GetShinStr()
  • GetXParency()
Każdy powinien wiedzieć co oznaczają poszczególne składowe koloru materiału. Jeśli tego nie wie, to niech zaglądnie do odpowiednich tutków w działach Direct3D/OpenGL (w zależności od tego które API woli). Ostatnia funkcja zwraca oczywiście przezroczystość. Jednak należy tutaj uważać - to nie jest kanał aplha. Znaczy można go tak traktować, ale tutaj 1 oznacza całkowicie przezroczysty. Tak więc, aby otrzymać kanał alpha należy do niego przypisać 1-GetXParency() . Po co więc mamy rzutować klasę Mtl na np. StdMat, skoro wszystkie parametry i tak możemy wyciągnąć bez rzutowania? Otóż szybko się okaże, że nie wszystkie. A co z teksturami? Na różnych materiałach tekstury są w różnych polach (np. jako mapa displacement, albo jakieś inne ciekawe). My jednak nie możemy używać tekstury tak jam Max. Dlatego potrzebujemy zrzutować sobie powiedzmy na StdMat. Po takim zrzutowaniu możemy już pobrać sobie tekstury. Są one zapisane w tablicy, której rozmiar zwraca nam metoda materiału NumSubTexmap(), a dostęp do poszczególnych texmap uzyskamy poprzez metodę GetSubTexmap(index). Należy jednak zauważyć, że ta tablica jest typu Texmap, a to nie oznacza jeszcze tekstury. Max ma przecież najróżniejsze dziwne rzeczy, które można nałożyć na dany kanał - np. noise map. My jednak nie potrafimy czegoś takiego odwzorować na obiekcie. Dlatego musimy sprawdzić, czy dana texmapa jest bitmapą. Robimy to już standardowo - poprzez Class_ID, czyli piszemy
if (Texmap->ClassID()==Class_ID(BMTEX_CLASS_ID, 0)) ...
Celowo podaję klasę Texmap w wywołaniu, a nie jakąś zmienną, aby wszyscy widzieli skąd co :). Oczywiście BMTEX_CLASS_ID to identyfikator definiujący teksturę. Tak więc jeśli już wiemy, że dana texmapa jest teksturą możemy pobrać jej dane. Musimy jednak jeszcze wiedzieć na jaki kanał została nałożona. Może to być przecież bitmapa dla kanału bump, diffuse, displacement i reszty :). Nas interesuje głównie kanał diffuse. Ponieważ sprawdzamy tekstury przypisane do StdMat (nawet jeśli to jest MultiMtl, to i tak przecież tekstury są w nim przypisane poprzez składowe materiały, które są typu StdMat - znaczy my zakładamy, że takich będziemy używać, bo innych nie potrafimy :) więc tak się składa, że numer w tablicy texmap dla materiału jest jednocześnie numerem kanału na który została nałożona tekstura. W pliku stdmat.h zostały zdefiniowane indeksy dla tejże tablicy. I teraz mając indeks tablicy możemy sobie sprawdzić jaki to kanał poprzez proste porównanie. Nazwa kanału diffuse to ID_DI. Jeśli potrzebujecie innych, to polecam zaglądnąć do pliku .h.

No ale dobra - mamy już materiał, mamy teksturę nawet, ale jednak nadal nie możemy tej tekstury nałożyć na obiekt, bo nie mamy współrzędnych mapowania. Z nimi na szczęście nie ma większego problemu ponieważ znajdują się one w dwóch tablicach siatki. Ich wyciąganie wygląda praktycznie dokładnie jak wyciąganie współrzędnych wierzchołków. Mamy bowiem tablice tVerts zawierającą współrzędne teksturowania oraz tablice tvFace zawierającą indeksy do tej pierwszej tablicy. Ilość wierzchołków teksturowania można pobrać za pomocą metody getNumTVerts() (małe g ! ;), a rozmiar tablicy tvFace jest taki sam, jak rozmiar tablicy faces. To prowadzi do oczywistego wniosku, że aby pobrać współrzędne tekstur dla danego face'a należy zrobić dokładnie to samo co w przypadku pobierania współrzędnych z tą różnicą, że teraz nie ma tablicy v dla face'a, ale jest tablica t :))). No i oczywiście zanim pobierzemy jakiekolwiek współrzędne teksturowania należy sprawdzić, czy one w ogóle są poprzez wywołanie getNumTVerts(). A kiedy są współrzędne? Ano kiedy zaznaczymy pole generate mapping coordinates w parametrach obiektu, albo kiedy nakażemy pokazywanie tekstury materiału na obiekcie w oknie renderowania (w czasie rzeczywistym). Tutaj warto nadmienić, że istnieją obiekty, w przypadku których takie proste wyciągnięcie współrzędnych nie wystarcza. Są to obiekty, których pierwszy i ostatni wierzchołek pokrywają się (np. kula, torus, cylinder). Należy sprawdzać takie sytuacje i powielać te wierzchołki.

Pozostała jeszcze jedna ważna rzecz. Otóż Max tak generuje współrzędne tekstury dla obiektów, aby można było na nie nałożyć pojedynczą teksturę - znaczy współrzędne są zawsze z zakresu [0, 1]. Aby móc powielić sobie teksturę większą ilość razy należy już użyć materiału, a w zasadzie już klasy BitmapTex (którą otrzymamy po zrzutowaniu klasy Texmap - oczywiście pod warunkiem, że ze sprawdzenia Class_ID wyszło nam, że texmapa jest rzeczywiście teksturą :). Z tej klasy należy pobrać wskaźnik na strukturę StdUVGen (za pomocą metody GetUVGen()). Struktura ta dostarcza nam kilka przydatnych parametrów takich jak skala (scale) i przesunięcie (offset). Skala oznacza ile razy powielamy teksturę na obiekcie (należy wymnożyć podstawowe współrzędne obiektu przez podaną wartość), a przesunięcie oznacza oczywiście przesunięcie tekstury względem podstawowego ustawienia (należy dodać je do współrzędnych). Skalę dla współrzędnej U pobieramy poprzez metodę GetUScl(0) (wszędzie jest ten czas :). Analogicznie dla V istnieje metoda GetVScl(0). Natomiast offset możemy pobrać poprzez metody GetUOffs(0) i GetVOffs(0).

Jeszcze chciałbym tylko nadmienić o jednej rzeczy, która jest raczej estetyczna. Otóż na pewno zauważyliście, że eksportując coś z 3D Studio Max na dole (na pasku Max'a) pojawia się pasek postępu i przycisk Cancel. Oczywiście my też możemy zrobić coś takiego. Do wystartowania paska służy metoda interfejsu (Interface) o nazwie ProgressStart(). Przyjmuje ona kilka parametrów, które są opisane w SDK więc nie będę tego robił. Aby update'ować pasek należy wywołać metodę ProgresUpdate(), do której trzeba przesłać wartość całkowitą (w procentach - oczywiście bez znaczka % :P). No i na koniec należy jeszcze zakończyć działanie paska. Służy do tego metoda ProgressEnd(). Podczas pracy paska pojawia się jeszcze jedna rzecz - przycisk Cancel. Aby sprawdzić, czy użytkownik nacisnął na tenże przycisk podczas eksportu należy wywołać metodę interfejsu GetCancel(), która oczywiście zwróci true jeśli przycisk został naciśnięty.

I to wszystko na dzisiaj. Wydaje mi się, że teraz już nikt nie powinien mieć problemów z eksportowaniem geometrii. Dzięki tej wiedzy i SDK wszystko stanie się prostsze i łatwiejsze. My się jeszcze oczywiście zobaczymy w temacie związanym z eksportem, bo następnym razem omówię glXPortera i format glX, czyli eksporter i format zapisu pliku, który stworzyłem na potrzeby działu OpenGL (dzięki temu nie będziemy już czuć się gorsi, że dalej mamy tylko jedną małą, biedną kostkę :PPP).

©Copyright by Domino   



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 ::