Wyświetlanie pojedynczych trójkątów – nawet najpiękniej pokolorowanych – to oczywiście dopiero początek. Niezależnie od tego, czy uczymy się obsługi jakiejś biblioteki 3D czy też piszemy własną, chcemy zająć się przede wszystkim wyświetlaniem brył. Rodzajów możliwych brył jest oczywiście sporo, ale te często używane i “regularne” ich rodzaje charakteryzują się przede wszystkim tym, że dają się opisać równaniami matematycznymi i zależnościami geometrycznymi. Wiemy na przykład, że sfera jest taką figurą, która składa się ze wszystkich punktów znajdujących się dokładnie w określonej odległości od danego – środka.
Problem w tym, że takich punktów jest “dość” dużo, bo nieprzeliczalnie wiele. Zarówno tą, jak i każdą inną bryłę w tradycyjnym renderingu zwykło się więc przybliżać automatycznie generowanymi trójkątami (jest to różnica np. w stosunku do raytracingu, czyli śledzenia promieni). Nazywa się to triangulacją i wymaga kilku operacji, takich jak:
I to w zasadzie wszystko. Warto oczywiście natychmiast sprawdzić rezultat przy pomocy renderowania w trybie wireframe, czyli rysowania tylko konturów figur. Jeśli wszystko dobrze pójdzie, to można się już pochwalić czymś więcej niż tylko jednym trójkątem, co też niniejszym czynię :)
Zawsze jest coś do zakodzenia. Nawet jeśli żadne potężne siły zewnętrzne niczego nam nie narzucają, to mimo to (czy raczej: właśnie dlatego) pod czaszką ciągle kołaczą się pomysły. Te co lepsze przejdą w końcu do stadium projektów, by wreszcie – w nielicznym gronie – dojść do tego momentu, w którym należy otworzyć swoje ulubione środowisko programistyczne i zacząć pisać.
Uważa się powszechnie, co poparto (zbyt) wieloma przykładami, że projekty ciężko jest kończyć. Jednak związana z tym aktywna czynność – czyli porzucenie – przychodzi oczywiście bardzo, bardzo łatwo. Tak naprawdę o wiele trudniej jest zacząć i choć wiele osób mówi, że wystarczy to zrobić “tak po prostu”, w istocie sprawa jest chyba znacznie bardziej skomplikowana.
Niezależnie bowiem od tego, czy wypływamy na szerokie wody kodu, mając w głowie jedynie pomysł, czy też posiadamy dokładny plan; i niezależnie od tego, czy już zabieramy się za programowanie, czy jeszcze zajmujemy się samym projektowaniem – w każdym przypadku musi nadać swojemu dziełu zupełnie nową formę. Opieramy się wprawdzie na notatkach, szkicach, w ostateczności tym co zostało na jeszcze w głowie – lecz musimy to wszystko przerobić na całkowicie inną postać.
Można więc powiedzieć, że zaczynamy from scratch – od zera. Ta pustka ma najczęściej bardzo konkretny wymiar: dużego, ascetycznego okienka z uporczywie migającym kursorem, żądającym, abyśmy te wolne miejsce czym prędzej zapełnili. Dla mnie jej widok jest zawsze rozpraszający i nawet jeśli akurat dokładnie wiem, co chcę napisać, zbija mnie to z tropu.
W takiej sytuacji przydatne jest posiadanie jakiegoś stałego, “magicznego” szablonu, nagłówka lub jakiekolwiek innego kawałka kodu – niekoniecznie komentarza – od którego możemy zacząć. Choćby nazwa pliku, doskonale nam znane imię autora, data, nazwa projektu, krótki opis pliku, niezbędne dyrektywy dla kompilatora… Coś, co na pewno nie sprawi, że nasz kod będzie lepszy, łatwiejszy do zrozumienia czy efektywniejszy. Ale przynajmniej pozwoli zacząć go pisać.
Gdyby zorganizować konkurs na jak największą ilość kodu napisaną celem osiągnięcia jak najprostszego efektu, to pewnie obrazek po lewej (i kod, który za nim stoi) mógłby zająć w nim całkiem dobrą lokatę. Na pierwszy rzut oka to tylko niebieski trójkąt na żółtym tle; na drugi, trzeci i każdy następny zresztą też :) Na tym arcyprostym obrazku nie widać całego, dość skomplikowanego mechanizmu, dzięki któremu możemy go oglądać.
Rzeczony trójkąt jest bowiem efektem pracy programowego renderera, którego to od jakiegoś czasu – z konieczności, acz nie bez pewnej przyjemności – staram się popełnić. Taki kawałek oprogramowania ma za zadanie robić mniej więcej to, co potrafią zaawansowane biblioteki graficzne w rodzaju DirectX i OpenGL. Są oczywiście istotne różnice, wśród których największą jest brak wykorzystania typowych możliwości współczesnych kart graficznych – czyli właśnie przetwarzania trójkątów. Wręcz przeciwnie: wszystkie obliczenia pracowicie wykonuje główny procesor, zajmując się po kolei nie tylko każdym wielokątem, ale także każdym pikselem. Ma więc wyjątkowo dużo roboty, z którą jednak potrafi sobie poradzić.
O czym świadczy więc pokazany tutaj trójkąt? Ano o tym, że podstawowy potok renderowana ma się całkiem dobrze. W jego skład wchodzi transformowanie trójkątów przekształceniami macierzowymi, oświetlenie per-vertex, sprawdzanie widoczności pikseli przy pomocy bufora Z oraz rzutowanie perspektywiczne i rasteryzacja wynikowej płaskiej geometrii. Zgadza się, to zupełne podstawy podstaw, nieobejmujące chociażby teksturowania, lecz i tak realizujący je kod nie wiadomo kiedy rozrósł się do ponad dwóch tysięcy linijek. Faktycznie więc to był dosyć pracochłonny trójkąt :)
Zabłysnę jeszcze przykładowym kodem wykorzystującym renderer, który to wyglądać może mniej więcej tak:
Inspiracje pewną popularną biblioteką w zakresie interfejsu są, jak sądzę, doskonale widoczne :)
Komentarze umieszczamy w kodzie, aby opisać jego działanie. Są one przeznaczone wyłącznie dla osób czytających go i jedynie programy typu javadoc czy doxygen – służące automatycznemu generowaniu dokumentacji – mogą się niektórymi komentarzami interesować. Na pewno jednak nie robi tego kompilator.
Wymyślono jednak, że niektóre elementy kodu potrzebują innych, specyficznych “komentarzy”, przeznaczonych dla kompilatora właśnie. Różnie się one nazywają w różnych językach, ale ich głównym celem jest przekazanie dodatkowych informacji odnośnie klasy, metody, typu czy innego elementu programu, bez potrzeby stosowania.
W .NET takie dodatki nazywa się atrybutami i umieszcza przed wybraną deklaracją, w nawiasach kwadratowych (lub kątowych w przypadku Visual Basica), oddzielone przecinkami. Atrybuty te mogą dotyczyć właściwie wszystkiego, począwszy od wskazania na klasę, która może być serializowana (i pola, które nie powinny być) po informacje dla Form Designera na temat danej właściwości niestandardowego komponentu:
Ogólnie dotyczą one jednak różnych funkcji samej platformy .NET i służą wskazaniu, które elementy pełnią określone role w różnych rozwiązaniach, które działają w jej ramach. Można aczkolwiek definiować także własne atrybuty, a potem w czasie działania programu wydobywać o nich informacje przy pomocy mechanizmu refleksji.
A jak to wygląda w Javie? Otóż tam od wersji 1.5 istnieją adnotacje. Ich nazwy poprzedza się znakiem @
i umieszcza w osobnych linijkach, poprzedzających deklaracje, których dotyczą. Ponieważ tutaj język jest ściśle związany z platformą (maszyną wirtualną Javy), adnotacje czasami pełnią funkcje “brakujących” słów kluczowych lub dyrektyw dla kompilatora. Typowy przykład to adnotacja Override
:
którą możemy oznaczać przesłonięte wersje metod wirtualnych. Jest to praktyczne, gdyż w przypadku popełnienia błędu (np. literówki w nazwie) kompilator ostrzeże nas, że tak naprawdę zdefiniowaliśmy zupełnie nową metodę (bez tego błąd objawiłby się dopiero nieprawidłowym działaniem programu).
Naturalnie, możliwe jest też tworzenie własnych adnotacji oraz pobieranie informacji o nich przy pomocy refleksji. Aż korci, żeby sprawdzić, kto od kogo ściągał tutaj pomysły ;-)
W C++ deklaracje standardowo nie mają żadnych “ozdobników”, ale pod tym względem w różnych kompilatorach bywa różnie. Na przykład w Visual C++ mamy słówko __declspec
, które służy do całego mnóstwo różnych celów. Wśród nich są chociażby takie oto warianty:
__declspec(align(n))
służy do określania, jak dane (np. pola struktur) mają być wyrównane w pamięci. Dzięki temu będą one umieszczone tak, by zajmowały zawsze wielokrotność podanych n bajtów, co przy odpowiedniej wartości (np. 32) może zwiększyć wydajność lub (dla 1) zmniejszyć zajętość pamięci.__declspec(deprecated)
pozwala oznaczyć dany element kodu jako przestarzały. Jego użycie będzie skutkowało wtedy ostrzeżeniem kompilatora.__declspec(dllexport)
i __declspec(dllimport)
służą do tworzenia symboli eksportowanych w bibliotece DLL i do importowania tych symboli w innym programie.__declspec(property)
wprowadza konstrukcję właściwości do klasy, bardzo podobną do tych obecnych w Delphi. Po podaniu jednej lub dwóch metod dostępowych (do odczytu i ew. zapisu), otrzymujemy właściwość o danej nazwie i typie. Jaka szkoda, że to nieprzenośne :)Zasadniczo Visual C++ posiada też atrybuty podobne do .NETowych, które są konieczne do tworzenia interfejsów COM. Na szczęście nimi, jak i samym COM-em, nie trzeba już sobie zaprzątać głowy :)
Kilka dni temu doczekałem się w końcu materiałów w IV Ogólnopolskiej Konferencji Inżynierii Gier Komputerowej, która odbyła w Siedlcach w kwietniu zeszłego roku. W ich skład wchodzą między innymi teksty referatów, jakie zostały tam wygłoszone. Wprawdzie wśród nich nie ma tym razem żadnego referatu mojego autorstwa, ale i tak były one bardzo ciekawe ;-)
Przeglądając je, zauważyłem też, że reprezentują one całkiem niezły poziom językowy. Nie jest to oczywiście sprawa najważniejsza (w końcu o wiele istotniejsza jest zawartość merytoryczna), ale brak dbałości o poprawność dość często idzie w parze z niską jakością tekstu także według innych kryteriów. Wciąż zresztą zdarzają się pojawiać na rynku książki informatyczne, których redaktorzy “położyli” sprawę niemal pod każdym względem: od tłumaczenia po korektę.
Odpowiednie tłumaczenie to często – w przypadku publikacji z tej branży – decyzja, które terminy pozostawić w oryginalnej formie. Akurat w przypadku programowania gier bardzo dużo z nich nie tylko jest używanych wyłącznie w formie angielskiej, ale wręcz nie ma dobrych polskich odpowiedników (jako najlepszy przykład zawsze podaję ‘shader’). To sprawia, że teksty o tej tematyce są prawie zawsze naszpikowane obcojęzycznymi wyrazami, które jeszcze często podlegają odmianie tak samo, jak inne słowa. A to rodzi mnóstwo problemów natury ortograficzno-edytorskiej, jakich można uniknąć, jeżeli pamiętamy o kilku zasadach:
Nie są to jak widać specjalnie skomplikowane reguły, a stosując się do nich, powiększamy coraz bardziej zagrożony obszar poprawności językowej w Internecie i nie tylko.
Unicode to ciekawy wynalazek. Zamiast stosować wymyślne sposoby na “przełączanie” sposobu interpretowania zwykłych 8-bitowych znaków, ktoś mądry wymyślił po prostu, że obecnie nie ma większych przeciwwskazań, aby zwykły tekst zajmował dwa razy więcej miejsca niż dotychczas. Powstał więc standard UTF-16, w którym stron kodowych nie ma, a każdy znak jest zapisany za pomocą jednego z 65536 kodów.
Ale jak to zwykle bywa z rozwiązaniami, które mają rozwiązywać istniejące problemy, Unicode natychmiast stworzył swoje własne :) Oprócz tego, że założone 16 bitów szybko okazało się za małe (stąd istnienie także UTF-32), powstał też szereg kłopotów praktycznych. Jednym z nich jest choćby to, że żyjemy w okresie przejściowym (który zresztą trwa już wybitnie długo) i że w użyciu jest zarówno unikod, jak i zwykłe ANSI. A na pierwszy rzut oka (ludzkiego i programowego) tekst jest po prostu ciągiem bajtów i bez zewnętrznych wskazówek nie jest możliwe określenie w stu procentach, czy został on zapisany w ANSI, UTF-8, UTF-16 czy UTF-32. Co więcej, znaki Unicode są oczywiście liczbami, a ponieważ zasadniczo zajmują one więcej niż jeden bajt, pojawia się problem z ustaleniem właściwej kolejności tych bajtów (little-endian lub big-endian).
Oba te problemy ma rozwiązywać tzw. znacznik porządku bajtów (Byte Order Mark), ale oczywiście jak większość dobrych praktyk, także i jego umieszczanie na początku dokumentów nie jest zbyt popularne :)
Z programistycznego punktu widzenia sprawa nie wygląda aczkolwiek aż tak źle i w większości przypadków jesteśmy w jednej z dwóch sytuacji. Pierwsza z nich to “nieświadome” korzystanie z unikodu (czy może raczej “szerokich”, dwubajtowych znaków), bo został o niego oparty używany przez nas język oraz platforma; najlepszymi przykładami są tu .NET i C# oraz Java.
Druga sytuacja to możliwość wyboru, z jakiego systemu będziemy korzystali. Bardzo dobre jest to, że prawie zawsze możemy zdecydować się na… oba, czyli potencjalne kompilowanie dwóch wersji: ANSI i Unicode. Pod Windows na przykład w programach pisanych w C++ wystarczy przestrzegać kilku prostych i dobrze znanych zasad:
char
(lub wchar_t
) należy – wszędzie tam, gdzie chodzi nam o znaki – używać typu TCHAR
, który w zależności od wersji zostanie zamieniony na jeden z tych dwóch.TEXT
(czyli TEXT("Coś")
zamiast po prostu "Coś"
), dzięki czemu zostaną one skompilowane jako łańcuchy ANSI lub Unicode.Niestety, słowo ‘niektóre’ sytuuje się dość daleko od ‘wszystkie’ i dlatego ostatecznie nie jest tak różowo. Czołowe miejsce na liście “niewspółpracujących” zajmuje standardowa biblioteka C++. Z nią trzeba sobie poradzić we własnym zakresie.
Nie jest to aczkolwiek bardzo trudne, jako że każdy kompilator definiuje makro dla rozróżnienia wersji ANSI i Unicode. Visual C++ na przykład włącza w tym przypadku symbol _UNICODE
, którego możemy użyć do stworzenia odpowiednich aliasów:
Możemy je umieścić we własnym pliku nagłówkowym i dołączać we własnych projektach. Ponieważ jednak typów zależnych od znaków jest w STL całkiem sporo, można z powodzeniem użyć chociażby rozwiązania zamieszczonego na CodeProject, w razie potrzeby rozszerzając je o kolejne aliasy.
Nie ma systemów bezproblemowych, więc po kilkutygodniowym “miodowym” okresie z Vistą zdarzył mi się w końcu pewien drobny problem. Nie był on specjalnie kłopotliwy i w gruncie rzeczy należał do zabawnych… gdyby nie to, że zdecydowanie zbyt dużo działo się tu samo.
Zacznijmy od tego, że – jak powszechnie wiadomo – w zasobniku systemowym (system tray) widoczne są między innymi ikonki dotyczące dźwięku i głośności oraz sieci i łączności. Ta pierwsza pokazuje z grubsza aktualną głośność – lub ewentualne wyciszenie, zaś druga stan sieci i jej aktywność. Dobrze jest więc mieć je na wierzchu.
Dlatego też niespecjalnie spodobało mi się, gdy pewnego dnia podczas uruchamiania systemu nie pojawiły się one na pasku tak jak zwykle. Ponieważ ich widoczność można kontrolować, otworzyłem okienko Właściwości paska zadań, lecz ku mojemu zdziwieniu odpowiednie pola wyboru okazały się nie tylko odznaczone, ale też… nieaktywne.
Oczywiście standardowe magiczne inkantacje stosowane w takich wypadkach – czyli restart systemu oraz sprawdzenie, czy wszystkie aktualizacje są zainstalowane – nie przyniosły rezultatu. Podobnie konsultacja z bardziej doświadczonymi użytkownikami systemu (cześć Asmodeusz :)) – głównie dlatego, że perspektywa przywracania stanu komputera sprzed tygodnia nie przedstawiała się szczególnie zachęcająco :)
Tym czego nie zrobiłem, było przeszukanie zasobów Internetu w poszukiwaniu rozwiązania. Okazało się to jednak zbytecznie, że problem ten naprawił się sam, i to nawet nie bardzo wiadomo kiedy. Przypadkiem zajrzałem po prostu ponownie do okienka Właściwości paska zadań i menu Start, żeby stwierdzić, iż pola wyboru Głośność i Sieć są wprawdzie nadal oznaczone, ale już jak najbardziej dostępne.
Z jednej strony to dobrze, że problemy które pojawiają się nagle i nie wiadomo skąd, nie wymagają samodzielnego rozwiązywania. Z drugiej strony wypadałoby wiedzieć, jakie są ich przyczyny. Tego nie udało mi się ustalić, ale wykonana poniewczasie analiza wyników wyszukiwania w sieci wykazała, że w razie nawrotów choroby należy wykonać następujące kroki:
IconStreams
oraz PastIconStreams
, aż szukana fraza nie będzie już występowała w Rejestrze.Cóż, jeśli genialna Vista potrafiła to wszystko wykonać z własnej inicjatywy, to wypada tylko być pełnym podziwu :) Podejrzewam jednak, że zarówno w tajemnicze pojawienie się, jak i zniknięcie problemu, zamieszane są nieznane siły wyższe, których pochodzenia nigdy nie poznamy. Są po prostu takie rzeczy, które się nie śniły filozofom i informatykom – zwłaszcza w odniesieniu do Windows ;P