Zaprogramowanie w miarę porządnego, elastycznego i wydajnego silnika grafiki 3D jest oczywiście dość trudne. W końcu trzeba zadbać o właściwą organizację sceny, wczytywanie modeli, system materiałów, oświetlenie, cienie i pewnie jeszcze mnóstwo innych rzeczy. Trzeci wymiar nie ułatwia nam wcale życia.
Wydawałoby się więc, że gdy go usuniemy, sprawy nabiorą znacznie przyjemniejszego (i prostszego) kształtu. No cóż, kiedyś życie było łatwiejsze – ale głównie dlatego, że mieliśmy mniejsze wymagania. Mieliśmy na przykład całkiem znośną bibliotekę 2D zaimplementowaną jako część DirectXa – czyli DirectDraw. Chociaż obcowanie z nią na początku mogło być dość odstręczające (zwłaszcza jeśli wcześniej korzystało np. tylko z windowsowego GDI czy arcyprostego SDL), to jednak i tak stworzenie powierzchni do rysowania z podwójnych buforowaniem jest w DD znacznie prostsze niż chociażby poprawne zainicjowanie urządzenia Direct3D…
Fakty są jednak takie, że 3D ma się dobrze i coraz lepiej, a grafika dwuwymiarowa jest najczęściej tylko dodatkiem (prawda, że niezbędnym) do renderowanej sceny. Dlatego też najlepiej używać do niej tego samego DirectXa, z którego korzystamy w trakcie wyświetlania scen.
I chociaż jest to generalnie trudniejsze, daje o wiele większe możliwości i lepszą wydajność. Można oczywiście korzystać z należącego do D3DX interfejsu ID3DXSprite, lecz jego możliwości są raczej ograniczone i do wygodnego stosowania należałoby zapakować go w bardziej gustowne klasy.
Stąd też lepiej, w moim odczuciu samemu zatroszczyć się o odpowiednie funkcje i klasy do obsługi rysowania 2D. Muszę przy tym przyznać, że jestem trochę może za bardzo przyzwyczajony do eleganckich interfejsów bibliotek w rodzaju GDI+, VCL czy Graphics32. Chodzi tu przede wszystkim o dosyć intuicyjną koncepcję “płótna” (canvas), czyli obiektu kontrolującego całe rysowanie (w zwykłym GDI nazywa się to kontekstem urządzenia). Płótno, jak sama nazwa wskazuje, jest to taki obiekt, na którym rysujemy inne obiekty – jak na przykład obrazy czy tekst. Jest to nieco inne rozwiązanie projektowe niż chociażby te stosowane w D3DX, w którym to same obiekty (sprite’y, czcionki, itp.) wiedzą, jak się narysować.
Jak zapewne nietrudno się domyślić, piszę o tym dlatego, iż obecnie zajmuję się właśnie implementacją modułu grafiki 2D opartego z grubsza o podane wyżej założenia. Naturalnie, zakodowanie wszystkich funkcji dostępnych w GDI(+) itp. byłoby strasznie pracochłonne i w większości przypadków zupełnie niepotrzebne. Dlatego ograniczam się przede wszystkim do wyświetlania prostokątnych obrazków, a później także do wypisywania tekstu.
Zadanie to może wydawać się proste, ale w gruncie rzeczy wcale takie nie jest. Weźmy chociażby pod uwagę to, że dla efektywności należy podczas rysowania obrazków (będących oczywiście oteksturowanymi prostokątami) unikać częstego przełączania tekstur. A z tego wynika, że pojedynczy “obrazek” to tak naprawdę określony fragment pewnej tekstury, dla którego trzeba chociażby wygenerować określone współrzędne. A to już wymaga odpowiedniego opisu takich obrazków (konsekwentnie unikam słowa sprite, które uważam za sztuczne ;P) – na przykład w zewnętrznym pliku tekstowym. Dla wygody czy chociażby dla celów animacji obrazki wypadałoby ponadto układać w kolekcje. W sumie wychodzi więc całkiem sporo pracy, nieprawdaż? :)
Zresztą samo rysowanie też nie należy do najłatwiejszych. Jedną z pojawiających się tu kwestii jest na przykład przycinanie (clipping), czyli ograniczenie rysowania do wybranego obszaru – w najprostszej wersji prostokąta. Direct3D od wersji 9 posiada wprawdzie narzędzie nazywane scissor test (“test nożyczkowy”), które to właśnie robi, jednak jego użycie praktycznie wykluczałoby możliwość buforowania rysowanych kształtów. Znaczy to, że każda zmiana obszaru przycinania wymuszałaby renderowanie zebranych trójkątów – co w dość skuteczny sposób obniżyłoby ogólną wydajność. Tak więc przycinanie trzeba robić – tadam! – ręcznie :P Na szczęście nie jest to bardzo trudne, jeżeli dotyczy wyłącznie prostokątów wyrównanych do osi układu współrzędnych (czyli wtedy, gdy wykluczamy obroty).
Tak mniej więcej przedstawia się początek tematu pt. grafika dwuwymiarowa. Czy ktoś nadal uważa, że jest to rzecz znacznie prostsza niż 3D? :)
Przesadzasz :)
Jeśli chodzi o generowanie współrzędnych obrazków w teksturze i zapisywanie ich do pliku tekstowego, to wydaje mi się to rozwiązanie trochę przesadne. Przełączania tekstur i tak się nie uniknie, pewnie, że można pogrupować jakoś obrazki (nawet należy) ale jeśli musisz generować dla nich współrzędne, to imho chyba za bardzo je upakowujesz. Bo w ilu miejscach w kodzie będziesz musiał później podawać te współprzędne? W jednym, w dwóch?
Jeśli chodzi o animacje, to ja np. układam sobie poszczególne klatki obok siebie poziomo i później w kodzie / skrypcie zwyczajnie mnożę sobie wielkość i dodaję do pierwszej współrzędnej. :P Ogólnie uważam, że stosujesz zbyt radykalne środki na małe problemy ale to tylko moje skromne lamerskie zdanie. :)
Przycinanie obrazków – nie łatwiej byłoby dostosować wzajemnie wielkość obszaru (np. 128 x 512) i tekstury (np. 32 x 32), żeby nie musieć przycinać? Pozdrawiam ;)
Nie przesadzam :)
Weźmy choćby takie GUI w grze. Na porządnie wyglądający skin składa się zwykle wiele bardzo małych elementów, np. ramka przycisku, znaczek X w checkboxie, itd. Każdy taki element może mieć kilkanaście na kilkanaście pikseli, a wszystko może się spokojnie mieścić na teksturze 512×512. A bez pakowania co będzie? Osiem przełączań tekstury żeby wygenerować edita? :D
Co do animacji, to jeśli mamy możliwość opisania obrazka jako dowolnego prostokąta tekstury, to tym bardziej można zrobić tak, jak mówisz :)
Co do przycinania – piszę się je raz, a dostosowywać by trzeba było w każdym przypadku.
Na usprawiedliwienie dodam, że i tak nie mam tylu dziwnych “udogodnień” jak np. Regedit, który pozwala tym prostokątom na teksturze zdefiniować dowolne przekształcenie za pomocą macierzy :D
A nie można tak?:
1. Ustaw teksturę
2. Narysuj wszystkie ramki (dla każdej kontrolki)
3. Ustaw teksturę
4. Narysuj wszystkie iksy
itd.
W ten sposób jest nie 8 przełączań tekstury na każdą kontrolkę, a 8 przełączań tekstury na narysowanie całego GUI.
Teraz z kolei Twoje rozwiązanie, Kurak, wydaje mi się przekombinowane :) Zgadza się z Xionem, że elementy GUI powinny trzymać się razem, dalej nie rozumiem po co generować współrzędne elementów do pliku tekstowego :P
kurak: To by strasznie skomplikowało rysowanie, jeżeli chcielibyśmy przy okazji zachować chociaż pozory OOPu :) Jest znacznie łatwiej jeśli każda kontrolka potrafi się narysować sama.
tarains: Dzięki temu można np. robić skiny dla GUI gdzie elementy mają różne rozmiary.
“To by było skomplikowane”? Przecież i tak podczas takiego odrysowywania kontrolki nie rysujesz np. ramki i iksa w tym samym momencie, tylko oddzielnie. Może i rozwiązanie niezbyt eleganckie ale wydaje mi się szybsze od odrysowywania każdej kontrolki osobno ;]
Jeśli każda kontrolka umie się sama narysować, to robisz tylko
ctl->Draw()
i cię nic więcej nie obchodzi. Gdybyś chciał globalnie dla całego widocznego akurat GUI grupować wszystkie tekstury, to trzeba by:
1. Przelecieć przez całe drzewo kontrolek.
2. Dla każdej kontrolki pobrać, jakich tekstur wymaga.
3. Dla każdej tekstury pobrać współrzędne wszystkich wierzchołków każdej kontrolki, które z tej tekstury korzystają.
Naturalnie – da się zrobić, ale wymagałoby to sporego dodatkowego interfejsu w klasie kontrolki skonstruowanego tylko po to, żeby wyliczać tekstury i zwracać koordynaty wierzchołków. Nie wspominając już o tym, że jak przypadkiem jakaś kontrolka potrzebowałyby czegoś specjalnego – na przykład zmiany renderstate’ów, multiteksturowania itp., to trzeba by to uwzględnić w interfejsie nie tylko tej kontrolki, ale takze:
– klasy bazowej kontrolek
– *wszystkich innych* kontrolek
Mówiąc wprost, cała hermetyzacja idzie się… eee, tzn. idzie sobie w las ;P
Nie, nie o to mi chodzi ;]
Owszem, można zrobić sobie po prostu “draw” – ale załózmy, że samym rysowaniem zajmuje się “rysownik”. Rysownik przechowuje tablice (wektory) – iksyDoRysowania, ramkiDoRysowania. Każda kontrolka ma wskaźnik do rysownika. I podczas rysowania najpierw dla każdej kontrolki wywołujesz draw().
draw kontrolki robi np. coś takiego:
rysownik->dodajIksDoRysowania(pozycja, obrót, skala);
rysownik->dodajRamkęDoRysowania(pozycja, obrót, skala);
Potem rysownik robi tak:
1. Ustaw teksturę iksów
2. Narysuj wszystkie iksy z tablicy
2. Ustaw teksturę ramek
3. Narysuj wszystkie ramki z tablicy
4. Wyczyść tablice ramek i iksów
Dodatkowo, jeśli nie da się usuwać rysowanych kontrolek, to tablice iksyDoRysowania i ramkiDoRysowania można sobie wypełnić przy inicjalizacji a potem tylko rysować z tablic, ew. dodawać następne elementy.
Żadnych współrzędnych wierzchołków się tutaj nie pobiera – tylko pozycja, obrót i skala względem domyślnej wielkości (np. 1, 1).
Dalej uważam że to lepsze rozwiązanie niż osobny draw dla każdego ;]
Ale wtedy nachodzące na siebie kontrolki (czy cokolwiek innego złożonego z kilku tekstur) będą dziwnie wyglądać.
Tarains: w końcu jakiś argument przeciw który muszę zaakceptować ;]
Mimo takich drobnych błędów (które na pewno też da się jakoś wyeliminować – wprowadzając chociażby głębokość) mój sposób na pewno jest prostszy niż pakowanie wszystkiego na jedną dużą teksturę ;]
Ale w pakowaniu chodzi tylko o elementy powiązane ze sobą, których rozdzielanie na mniejsze pliki nie miałoby sensu.
Rzeczywiście, zabawa z kolejkowaniem tekstur do narysowania i ustawianiem głębokości każdego elementu, by wyeliminować “drobne błędy” jest dużo prostsza. ;p
Tak, jest prostsza od łączenia w kodzie teoretycznie dowolnie wyglądających tekstur o dowolnych rozmiarach i generowania do tego koordynatów tekstury ;p
Ale jeśli przycisk ma wszystko spakowane na jednej teksturze, która sobie leży na dysku to nie widzę problemu. Jedyne, co mi się nie podoba, to pisanie kodu który ładuje obrazki na jedną teksturę.
Imho jak robisz GUI to masz zapewne rodzica( nadrzędna kontrolkę ) który może zcachować wszystkie Batche renderingu i pogrupować wedle materiałów :P i tak to się robi zazwyczaj :P