sobota, 27 września 2008

Apokalips - release

"Story in a game is like a story in a porn movie. It's expected to be there, but it's not that important."
("Fabuła [historia] w grach komputerowych jest jak fabuła w filmach pornograficznych. Powinna tam być, ale nie jest tak ważna.")

- John Carmack
Tym cytatem mógłbym rozpocząć notkę na temat wydania gry Apokalips. Doskonale wiem, że zwalą się na mnie gromy ludzi, którzy to testowali (albo i nie), ze pełno błędów, modele nie takie jak trzeba, mało dynamiki, za proste i "o co w tym w ogóle chodzi". Tym niemniej, stwierdziłem, iż nie ma co dłużej trzymać tego projektu, a lepiej zrzucić z siebie ten "balast" i zacząć coś innego (choć, cholera, studia się rozpoczynają...). Decyzja o wydaniu została też podyktowana dwoma względami. Po pierwsze, okazało się, że z lepszych modeli (a konkretnie lepiej oteksturowanych) będą nici i nawet zmiana formatu na MD2 nie pomagała. Po drugie natomiast, chciałem mieć zapas, w przypadku, gdybym zdecydował się wysłać gierkę na Warsztat Summer of Code 2008 organizowanym na Warsztacie.

A co do samej gry? Muszę przyznać, że nie jest za długa, ale może to być też zaleta - komu by się chciało przechodzić dłuższy produkt o podobnej tematyce. Zresztą, zasoby zajmują na tyle dużo miejsca, że strach pomyśleć o większym projekcie. Apokalips zbudowany jest na wersji frameworka, która była już gotowa na początku lipca 2008 roku. Jest to wyjaśnienie, dlaczego w projekcie nie ma ładnego oświetlenia, cieni i ładnego Frustum Culling. Nie postanowiłem też mieszać obu wersji - raz ze względu na czasochłonność, a dwa z uwagi na możliwe błędy. W każdym razie - jest to dla mnie nauczka na przyszłość. A przyznaję, że Apokalips traktowałem jako przetarcie się i projekt, który muszę skończyć, choć niekoniecznie z rewelacyjnymi rezultatami.

Więcej informacji technicznych znajdziecie tutaj, natomiast samą grę możecie ściągnąć stąd.

Przyszła pora na podziękowania. Dziękuję przede wszystkim tym, którzy testowali grę w większym stopniu (Deshirey, Epyon, Shirou Noir, a także całej reszcie), WM-owi za stworzenie części modeli i spróbowanie swych sił jako grafik 3D, Masklowi za wsparcie techniczne i wszystkim (z Martusią na czele) za wsparcie mentalne. Uch, troszkę górnolotnie to zabrzmiało, ale lubię pisać takie rzeczy. Prawie jakbym dostawał Oscara. Specjalne podziękowania jeszcze raz dla Deshireya, także za udostępnienie miejsca na serwerze.

W chwili obecnej nie chce nic więcej pisać, gdyż jeśli kogoś rzeczywiście ciekawi projekt, to sam ściągnie i zobaczy. Powinien uruchamiać się bez problemów, przynajmniej na Windowsie XP. Na Viście wielu osobom nie udało się odpalić Apokalipsu i tak naprawdę do dzisiaj nie wiem dlaczego.

Pozdrawiam i dziękuję - SceNtriC.

czwartek, 25 września 2008

Modelowanko i mapowanko

Powiem szczerze, że doszedłem do momentu w rozwoju frameworka, w którym już nie bardzo wiem, co dalej robić. Oczywiście, jest jeszcze naprawdę sporo podstawowych spraw, które powinien takie projekt posiadać, lecz nie mam zbytnio pomysłu, za którą się zabrać. Inne rzeczy z kolei są na razie chyba poza moim zasięgiem - mowa tu o takich zjawiskach jak parallax mapping, renderowanie wody, ray tracing czy inne. Nie ukrywam jednak, iż zamierzam się do tego dobrać w bliższej lub dalszej przyszłości.

Nie znaczy to jednak, że nic nowego się nie pojawiło. Przeciwnie - kolejne partie kodu błąkające się niegdyś po Ogrodzie Inwencji Twórczej znalazły swoją przystań w plikach z rozszerzeniami .h i .cpp. Znowu parami.

Modele MD2

Framework umożliwiał już wprawdzie dosyć szybkie renderowanie modeli w formacie OBJ, ale już na przykładzie Apokalipsu zauważyłem, że to nie jest do końca to, o co mi chodziło. Przede wszystkim, to rozszerzenie nie obsługuje animacji, przez co nie dało się stworzyć dynamicznej strzelanki (taaaaak - SceNtriC i dynamika...). Co prawda, na upartego dałoby się znaleźć rozwiązanie - stworzyć koło 200 plików OBJ tego samego modelu, lecz z odpowiednią przesuniętymi wierzchołkami i wyświetlać to jedno po drugim. Przyznacie jednak, że ten pomysł jest głupszy niż ustawa przewiduje - nie dość, że pracochłonne, małoefektywne, pamięciożerne, to by jeszcze sporo miejsca na dysku zajęło.

Dlatego zainteresowałem się przykładem, który pozwala na wczytywanie, animowanie i wyświetlanie tych samych modeli, ale w formacie MD2. Co bardziej doświadczeni czytelnicy tego devbloga pamiętają może, skąd to rozszerzenie się wywodzi. Oczywiście, powstało ono w głębokich lochach z kajdanami w Teksasie wraz z dowodzącym Wielkim Panem z Batem. Ten opis byłby udany, gdyby nie dwie istotne informacje - głębokie lochy to tak naprawdę studio id Software, w którym powstawał Quake 2, a Wielki Pan z Batem, to po prostu "niejaki" John Carmack. Teksas się zgadza.

Tak czy inaczej, od tej pory we frameworku zawarta jest podobna do COBJManager klasa CMD2Model. Różnica (poza oczywiście formatem modeli i korzyściami z tego płynących) jest taka, że pierwszy twór jednym obiektem obejmował niezliczoną ilość modeli (widać to zresztą po nazwie - "Manager"). CMD2Model natomiast stosuje zasadę "jeden obiekt to jeden model" - jest to konieczne ze względu na dużą większa ilość informacji, jaka jest do przetworzenia no i samą istotę animacji. Gdyby ktoś chciał, zrobiłby z tego klasę zarządzającą kilkoma takimi obiektami - nie ma jednak co utrudniać sobie życia.

A co z teksturami, które tak będą szwankowały w Apokalipsie? Cóż, tutaj zaprzęgnięty został format obrazów PCX i całość wygląda znośnie. Choć może nie na przykładzie ze screena...

Dot3 Bump Mapping

Z tą techniką chciałem zmierzyć się już dużo wcześniej. Niestety, brakowało mi podstawowej wiedzy o implementowaniu shaderów w OpenGL i przykładów. Co jak co, ale nie nadaję się nawet na początkującego znawcę wektorów i ich liczenia, toteż nie dziwota, iż trudno mi coś konstruktywnego tutaj zdziałać. Od tamtej pory trochę wody w rzekach upłynęło, mi urosły włosy, a mi się udało stworzyć coś, co chociaż przypomina mapowanie wypukłości.

Samego algorytmu nie omówię z wyżej przytoczonych względów. Zresztą wytłumaczenie oświetlenia per pixel tez mi ciężko szło. Mogę jedynie ze spokojem stwierdzić, że do normal mappingu (gdyż tak to się chyba nazywa) potrzebne są dwie tekstury - zwykła, którą wyświetlimy normalnie w programie, oraz mapa normalnych, którą możemy przygotować chociażby przy użyciu GIMP-a i której nie powinno się renderować (a przynajmniej ja nie sprawdzałem, co z tego wyjdzie - pewnie "kaszka z mleczkiem"). Oczywiście, potrzebne są też współrzędne oka kamery oraz pozycji światła. Z tymi (oraz macierzowymi) informacjami shader przekształca dane obiekty. Różnica jest widoczna na screenach, choć znowu troszkę zły przykład wybrałem.

Całość została włączona w skład klasy CLighting, której zadaniem jest tak naprawdę odpowiadać za wszystkie typy oświetlenie - nie tylko per pixel, ale także bump, czy w przyszłości (mam nadzieję) parallax.

Cóż, niedługo zbliża się termin skończenia Apokalipsu - niestety, okazało się, że modeli nie da się poprawić (dlatego też lekko zdenerwowany zająłem się formatem MD2, co i tak zbytnio nie pomogło). Co więcej, widzę znaczącą różnicę pomiędzy wersją frameworka, jaka obejmuje Apokalips, a tą obecną. Założyłem sobie jednak, że nie będę już na siłę mieszał poszczególnych wersji, przez co będzie wyglądało tak, jak zawsze - nudno i szaroburo. Trudno - przynajmniej mam chrzest bojowy.

Pozdrawiam i dziękuję - SceNtriC.

niedziela, 21 września 2008

Takie dziwne cosie i plamy na podłożu

Brak nowej notki mógłby wskazywać na fakt, iż od jakiegoś czasu nie piszę nic nowego. Wszystkim, którzy tak myślą, mogę powiedzieć tylko jedno "mylicie się". Po prostu uznałem, że nie ma sensu robić kilku krótszych notek, tylko poczekać na większą partię materiału do przemaglowania. Choć nie ukrywam, że co jakiś czas siłowo wyłączam komputer lub po prostu zajmuję się czymś innym - na przykład graniem (przy okazji - polecam nieśmiertelne Majesty i równie nieśmiertelne Heroes of Might and Magic 3 z nieoficjalnym dodatkiem In the Wake of Gods). Ostatnio też zająłem się koszykówką, choć jestem w niej na takim poziomie, jakim są fastfoodowcy w bieganiu na setkę. Niby typowy podkoszowy (jeśli chodzi o zachowanie się na boisku), ale niepredysponowany warunkami fizycznymi (186 cm wzrostu przy 69 kg wagi). Jednak mam chociaż długą brodę, mroczną aurę i peszę przeciwników możliwością odepchnięcia ich za kosz. Ech, zresztą nieważne...

Miałem pisać w końcu o tym, co się zmieniło w moim skromnym frameworku ostatnimi dniami (choć w sumie projekt rozrósł się do ponad 10000 linijek i 60 plików). A zatem kolejna para przed nami - kwaterniony i cienie.

Kwaterniony

Artykuł na zachętę - autorstwa Regedita (pozdrawiam).

Nie będę zaskoczony, gdy ktoś przyzna, że spotkał się z tym określeniem pierwszy raz. Ja też całkiem niedawno się o tym dowiedziałem i jedyne, co zrozumiałem, to fakt, iż nie zamierzam tego wykorzystywać. Zresztą sposób na kamerę 3D (a przy tym kwaterniony się przydają) znalazłem inny. Jeśli jednak można, a framework z kolejną partią matematyczną wyglądałby lepiej - dlaczegóż by nie?

Kwaterniony w matematyce są dosyć skomplikowanym tworem, wykorzystującym liczby urojone i ogólnie nie wyglądają zbyt mile dla oka. W ujęciu programistycznym są one odrobinkę bardziej przystępne. Generalnie, budową można je przyrównać do wektora z czterema współrzędnymi. Na tym jednak podobieństwo się kończy - wektory służą do położenia punktu w przestrzeni i zaznaczenie przesunięcia, natomiast kwaterniony definiują orientację punktu oraz obrót. Mają parę zalet w porównaniu z kątami Eulera, z czego chyba najważniejszą jest możliwość wykonywania dowolnych (nieblokowanych) obrotów w trójwymiarze. Nie zawsze to jest jednak pożądane.

Artykuł Rega spełnił tutaj dosyć istotną rolę - większość informacji z poprzedniego akapitu zagnieździła się w mojej głowie po przeczytaniu tekstu. I pozwolił on chociaż w części zrozumieć te twory, które mają tą zaletę, że mają groźną nazwę i od teraz będę ich używać częściej.

A sama implementacja, dzięki przykładowym kodom z artykułu, przebiegła chyba bez większych komplikacji. Jedynymi zawiłościami okazało się przystosowanie metod do ogólnej koncepcji frameworka (mniejsza ilość argumentów itd.) i parę pomniejszych przydatnych funkcji. Jedyna nieścisłość jest taka, że jeszcze struktury CQuaternion nie przetestowałem - liczę jednak, że tym razem prawa Murphy'ego nie okażą się dla mnie bezwzględne.

Cienie

Artykuł na zachętę - autorstwa Arne Olava Hallingstada (pozdrawiam).

Zawsze powtarzałem, że przykładowe kody źródłowe są najlepszych przyjacielem początkującego programisty - pozwalają szybko i bezboleśnie implementować pożądane efekty. Jedyny minus jes taki, że czasami lenistwo bierze w górę i nie do końca rozumie się rzeczy, które w takim kodzie się znajdują. Zwłaszcza, jak ktoś nie przerobił lekcji o buforze szablonowym, tak jak ja...

Tak czy inaczej, na screenie widać efekt zastosowania tzw. shadow mapy (nie wiem, czy pisze się to razem czy osobno - tych, co wiedzą, proszę o komentarz i przepraszam za ewentualny błąd. Tyczy się to też height map, sky dome'ów itd.). Nie jest to najlepsza technika w grafice 3D, ale na początek starczy. Polega ona na obliczeniu macierzy cieni w oparciu o wektory normalne do powierzchni i pozycji światła, a następnie przemnożeniu jej przez aktualną macierz modelowania. W międzyczasie robi się różne dziwne rzeczy z stencilem i renderuje się większość rzeczy dwa razy. Sam kod źródłowy nie był zbyt skomplikowany czy długi. Wypadało jednak zrobić wersję obiektową - troszkę myślenia było, ale generalnie całość polega na podawaniu wskaźników do funkcji, trzymaniem ich w wektorach z biblioteki STL, a następnie automatycznym renderowaniu sceny. Ogólnie kupa zabawy z przewagą tego pierwszego. Ale przynajmniej są cienie - dosyć chyba ważny element grafiki 3D. Niestety, póki co przećwiczony na jednej scenie i nie wiadomo, jak się sprawdzi przy np. wielu płaszczyznach, na które pada cień. Próby wypadły jednak na tyle obiecująco, że na razie zostawiam. A nuż (a widelec).

Na koniec chciałbym zachęcić do obejrzenia tego filmiku, który jest krótkim pokazem powstającego silnika 3D autorstwa Conrada (pozdrawiam). Rzadko się zdarza, aby coś mnie urzekło, a tu właśnie jest taki przypadek. Polecam (tylko link podaję, bo coś niektóre przeglądarki wysiadają przy embedach na blogach).

Pozdrawiam i dziękuję - SceNtriC.

środa, 17 września 2008

Okrawanie i losowanie

W chwili, gdy piszę wstęp do tej notki, na zegarze wyświetla się godzina 21:12 17. dnia września br. Powoli zbliża się moment, kiedy dostanę tzw. legitymację studencką i będę musiał wszem i wobec twierdzić, że studentem jestem nijakim - bądź co bądź, siedzę cały czas przed monitorem, coś tam kodzę, wymyślam, bujam w obłokach, gram, uczę się i z premedytacją pomijam wielką aktywność towarzyską. A do tego zapomniałem o Dniu Programisty - choć znając mój stosunek do "świąt"...

Dobrze, nieważne, narzekać będę innym razem. Przejdźmy do sedna wpisu, czyli "co nowego SceN przepis... ehem - zaprogramował w celu utworzenia ze swego frameworka dużego, silnego i uśmiechniętego noworodka".

Frustum Culling

Większość pewnie wie, o co chodzi. Mniej zorientowanym przetłumaczę w bardzo wolny sposób powyższe dwa słowa na nasz rodzimy język - niewyświetlanie niewidocznych obiektów. I wszystko powinno już być jasne. Technicznie rzecz biorąc, polega to na obliczaniu płaszczyzn określających bryłę obcinania, a następnie testowaniu każdego chętnego do renderowania elementu, czy się w takowej bryle mieści. Co ciekawe, sam kod nie jest za długi. Na dodatek pozyskałem kolejną matematyczną część frameworka, a mianowicie operacje związane z płaszczyznami w przestrzeni 3D - a nie wiem, czy ktoś pamiętam, jak męczyłem się z tym przy implementacji kolizji. Cóż - przynajmniej nie jestem tak zielony, jak w przypadku wektorów i (zwłaszcza) macierzy. Tak czy inaczej, przykład z UGP jest całkiem ładnie opisany i właściwie nie ma tu o czym mówić - dostałem kolejny oręż w walce z zamierającą szybkością działania aplikacji. Problem może być przy samym testowaniu renderowania, gdyż mechanizm wydaje mi się ciut nieintuicyjny, a mnie przede wszystkim chodzi o jak najłatwiejsze korzystanie z kodu. Nie wiem, jak ta technika jest rozwiązana w darmowych silnikach takich jak Ogre czy Irrlicht - mam nadzieję, że dużo od nich mój framework nie odstaje.

Generator liczb pseudolosowych

Czasami, po ciągłych próbach zrozumienia grafiki 3D, człowiek chce się zająć czymś normalniejszym. Problem w tym, że niekiedy ta odskocznia jest równie lub nawet bardziej "idiotopędna" od tego, od czego uciekamy. Dlaczego "idiotopędna"? Gdyż próbując się w nią zgłębić dochodzimy do wniosku, iż jesteśmy za głupi, ażeby ją pojąć, a jeśli nawet nam się to uda, to próbując o tym opowiedzieć kilku pierwszym osobom mamy 80% szans, że usłyszymy "muszę iść, na razie". Ciężki jest żywot programisty...
Już parę miesięcy temu, po zasłyszeniu zgodnych opinii z kilku różnych źródeł, iż losowość w Bibliotece Standardowej C++ (wielkimi literami, bo to w końcu obiekt kultu) po prostu gryzie kopiec kreta, zainteresowałem się algorytmami stworzonymi specjalnie do generowania liczb pseudolosowych. Pewne zatem było, że musiałem wpaść na Mersenne Twister, zaprojektowany w 1997 roku przez panów Matsumoto i Nishimura. Jest dosyć szybki, wygodny, przenośny i właściwie po znalezieniu go nie chciało mi się szukać innych (to główny powód, dlaczego się nim zainteresowałem). Niestety, wtedy moja wiedza programistyczna była na tyle wąska, że z żałością skasowałem przykład pobrany chwilę wcześniej z Internetu. Teraz, choć moja wiedza wcale większa nie jest, stwierdziłem, że mając w perspektywie zajęcie się cRPG-iem (kiedyś...), wypada pomyśleć nad implementacją Twistera w frameworku. Zagryzłem zęby i to zrobiłem - łącznie z losowaniem liczby naturalnej z danego przedziału (a co za to idzie - łatwą symulacją rzutu kostkami K4, K6, K8, K10, K12, K20 i K100). Właściwie nie wiem, co tutaj więcej napisać - postarałem się maksymalnie ułatwić korzystanie z generatora. Oczywiście, jeden generator to jeden obiekt klasy CRandom. Więcej informacji na mojej wikispacji.

Znowu się nie postarałem dzisiaj, a na dodatek strasznie zrzędziłem. Mam już niestety tak w zwyczaju - mam nadzieję, że nie przeszkodzi mi to w dalszej rozbudowie kodu.

Pozdrawiam i dziękuję - SceNtriC.

sobota, 13 września 2008

Szejderki też już są

Czasem się tak zdarza, że boisz się jakiegoś rozwiązania, bo go nie rozumiesz. Nie jest to w sumie nic zdrożnego - trzeba tylko umieć przypomnieć sobie o tej rzeczy później, jak wiesz już więcej, albo po prostu nie wiesz co robić. Tak pokrótce można opisać relację na linii shadery-SceNtriC. Innymi słowy - w końcu udało mi się napisać działającą klasę, która wykorzystuje moc procesora karty graficznej i nie jęczy, że jej czegoś brakuje. A co najważniejsze - uruchomiło się poprawnie. Można więc stwierdzić błąd zerowej kategorii - "Kompiluje się, działa, więc lecimy z nową notką na devbloga".

Nie jest to oczywiście nic nadzwyczajnego (zresztą zawsze tak mówię) - po prostu przeskoczyłem pewien etap rozwoju programisty grafiki 3D, czyli umiem analizować przykłady w Cg Toolkit. Właśnie ten język shaderów wykorzystałem - GLSL z jakiegoś powodu nie chce u mnie działać, z HLSL nie korzystam, bo w DX-ie jestem zielony niczym Gremlin z zamku Tower w Heroes 3, a shaderów assemblerowych jakoś nie mam ochoty się uczyć. Aż takim masochistą jeszcze nie jestem.

Tak czy inaczej, uznałem, iż wypadałoby się zająć czymś w rodzaju oświetlenia, którego dotąd w OpenGL unikałem ze względu na to, że nic w tej dziedzinie sam nie osiągnąłem z powodu małej wiedzy (innymi słowy - tutoriale były dla mnie za trudne). Nie da się jednak ukryć, że gra bez oświetlenia jest dosyć mało efektowna, a same lightmapy i tekstury z "błyskiem" nie zawsze się sprawdzają. Postanowiłem zatem wrócić do dwóch aspektów programowania, które mi sprawiały kłopoty i je połączyć w jednej klasie CLighting. Śmiem twierdzić, że się udało - światło działa, zarówno dla jednej, dwóch tekstur jak i jej braku. Przy okazji okazało się, że łączenie Cg z OpenGL wcale nie jest takie paskudne. W sumie wszystko skończyło się dobrze, szczęśliwie, a oświetlenie poszło i rozmnożyło się.

Właśnie - jakie oświetlenie? Nic odkrywczego - per pixel lighting moim zdaniem na początku sprawi się dobrze. Klasa została zaprojektowana w ten sposób, że w sumie dodawanie kolejnych efektów oświetlenia (stosuję zasadę, że jeden obiekt to jedno źródło jakiegoś światła) nie nastręczy większych trudności. Po prostu dojdzie parę nowych instrukcji warunkowych i samych plików shaderów.

Teraz troszkę techniki - zasada działania per pixel lighting i "jak to się robi w programie", czyli "jak-fajnie-że-framework-ma-nowe-możliwości-które-i-tak-plasują-go-lata-świetlne-za-innymi". Pozwolicie, że zajmę się tylko fragment (pixel) shaderem - jego odpowiednik dla wierzchołków jest do bólu standardowy.

Potrzebujemy przede wszystkim współrzędnych badanego piksela [Position], współrzędne jego wektora normalnego [Normal] (te dwie dane shader pobiera sobie praktycznie sam), współczynnik globalnego oświetlenia otaczającego [GlobalAmbient], kolor światła [LightColor], pozycję źródła światła [LightPosition], współrzędne oka kamery [EyePosition], współczynniki materiałowe dla odpowiednich rodzajów światła [Ke, Ka, Kd, Ks] oraz moc odbicia [Shininess]. Wszystkie te wartości oprócz ostatniej składają się najczęściej z tablicy trójelementowej, natomiast moc odbicia to pojedyncza wartość. Najpierw należy zadeklarować sobie dwa wektory, p i n. Pierwszy to praktycznie Position, a drugi to znormalizowany wektor normalny.

float3 p = Position.xyz;
float3 n = normalize(Normal);

Następnie obliczymy już dwa składniki końcowego wyniku (czyli koloru piksela). Będzie to emissive (można to przetłumaczyć w wolny sposób jako światło emisyjne) oraz ambient (światło otaczające).

float3 emissive = Ke; // to jest po prostu podany współczynnik Ke float3 ambient = Ka * GlobalAmbient; // współczynnik Ka przemnożony przez wartość globalnego światła otaczającego

Należy się teraz zająć światłem rozpraszającym (diffuse). Tutaj sytuacja wygląda trochę bardziej skomplikowanie. Potrzebny jest wektor l (znormalizowana różnica wektorów pozycji światła i współrzędnych piksela) oraz wartość cząstkowa światła. Jest to większa wartość spośród zera oraz iloczynu skalarnego wektorów l i n. Potem oblicza się iloczyn współczynnika Kd, koloru światła i obliczonej przed chwilą wartości. Mamy zatem trzecią cząstkową.
float3 l = normalize(LightPosition - p); float diffuseLight = max(dot(l, n), 0); float3 diffuse = Kd * LightColor * diffuseLight;

Pozostaje zatem światło odbijające (specular). Najpierw obliczamy wektor v, który jest różnicą wektorów pozycji oka kamery oraz piksela, a następnie go normalizujemy. Podobnie robimy z sumą wektorów l i v. Wartość cząstkowa światła to większa wartość spośród zera i iloczynu skalarnego wektorów h i n podniesiona do potęgi mocy odbicia. Uff, troszkę to dziwne, może zatem kod.

float3 v = normalize(EyePosition - p); float3 h = normalize(l + v); float specularLight = pow(max(dot(h, n), 0), Shininess); if (diffuseLight <= 0) specularLight = 0; float3 specular = Ks * LightColor * specularLight;

Ostatni etap jest najprostszy - polega on na obliczeniu współrzędnych x, y i z koloru danego piksela jako sumy wartości emissive, ambient, diffuse oraz specular.

Color.xyz = emissive + ambient + diffuse + specular; Color.w = 1; // czwarta współrzędna ma wartość 1

To jest wersja dla sceny bez tekstury. W innym przypadku, powyższą sumą należy jeszcze pomnożyć przez wartość teksela (koloru tekstury w danym punkcie).

Cóż, troszkę to zamotane. Przyznam uczciwie, że nie wszystko jest dla mnie do końca jasne. Ważne jednak, aby umieć z tego korzystać, a skoro taki mechanizm działa, to nic mi już nie potrzeba...

Pozdrawiam i dziękuję - SceNtriC.

wtorek, 9 września 2008

Zmacierzowane kości wektorowe

Kolejne dni mijają, coraz więcej rzeczy się odkłada i sam już nie wiem, gdzie ręce włożyć. Czteromiesięczne wakacje jednak źle na mnie działają...

W związku z zaistniałą sytuacją, spróbowałem włączyć w skład frameworka kolejny "ficzerek" (cholera, nie lubię używać neologizmów). W tym celu oczywiście opierałem się na kolejnym przykładzie z Ultimate Game Programming - jeszcze raz chwała autorom takich serwisów, że udostępniają kody źródłowe, z których można się uczyć.

Matematyka

Cóż, działania matematyczne zawsze się przydadzą. Zwłaszcza, jeśli są to wektory i macierze, które - nie da się ukryć - są głównym motywem tajemnych działań przeprowadzanych chytrze na procesorze karty graficznej i tym właściwym. Problem w tym, że tego typu przekształcenia dla większości osób nie są do końca intuicyjne - tu coś dodamy, tu przemnożymy, gdzie indziej coś przesuniemy, ogólnie - kupa zabawy. Tyle, że to nie może być zrobione tak sobie - dlatego na całe szczęście projektanci OpenGL-a i DirectX-a sami o to zadbali.

Ale jednak kiedyś wypada poznać chociażby podstawowe sztuczki rządzące prawami grafiki komputerowej. W tym celu we frameworku pojawiły się dwie struktury - CVector4 i CMatrix4x4, które w dodatku niesamowicie przydają się przy implementowaniu chociażby animacji szkieletowej. W sumie sam nie wiedziałem, że aż tyle operacji można wykonać na tych Elementach Składowych Algebry Liniowej (w skrócie ESAL), a to na pewno nie są wszystkie. Łącznie, kod tych dwóch rzeczy zajmuje 1256 linijek - całkiem sporo. Głównie za sprawą wielu przeciążonych operatorów, które umożliwiają takie "cuda na kiju" (oryginalne znaczenie tego powiedzenia dotyczyły Magic: the Gathering, a konkretnie postawionego na stół Isochron Sceptera + jakiś fajny czar typu instant - ech, do dzisiaj mam tą talię...):

CVector4 v1(1.0f, 0.0f, 2.0f); // wektor v1 o współrzędnych [1, 0, 2, 1]
CVector4 v2(1.0f, -1.0f, 0.0f); // wektor v2 o współrzędnych [1, -1, 0, 1]

v1.x += 2.0f; // teraz v1 jest [3, 0, 2, 1]

CVector4 v3; // wektor v3 o współrzędnych [0, 0, 0, 1]

v3.CrossProduct(v1, v2); // iloczyn wektorowy v1 i v2 zapisany w v3, które jest [2, 2, -1, 1]
CVector4 v4(2.0f, 2.0f, -1.0f, 1.0f); // wektor v4 o współrzędnych [2, 2, -1, 1]
if (v3 == v4) // jeśli v3 i v4 są równe (a są)
{

printf("Dziala, kurna!"); // optymistyczny napis z gatunku "hurra"
} CMatrix4x4 m1; // macierz jednostkowa
m1.Rotate(30.0f, 1.0f, 0.0f, 0.0f); // obrót macierzy o 30 stopni wzdłuż osi X

CMatrix4x4 m2(m1); // druga macierz będąca kopią pierwszej

m2.Translate(v3); // translacja drugiej macierzy o wektor v3


Zdaję sobie sprawę, iż tego typu rzeczy większość programistów grafiki jest w stanie podać z pamięci wraz z dokładnym umiejscowieniem danych macierzy do transformacji i że to nic wielkiego. No cóż, ale satysfakcja pozostaje. A poza tym - wszystkie frameworki/silniki to mają - mam i ja!

Animacja szkieletowa

Z terenem walczyłem jakiś tydzień, aby znaleźć literówkę. Aż bałem się myśleć, co mnie spotka tutaj. I co się okazało? No cóż, jestem zawiedziony - pomijając błędy składniowe, który chwycił komp(l)i(l/k)ator nie spotkało mnie nic złego oprócz nadmiernej czułości myszy (szkoda, że tylko myszki), co jest akurat winą GLUT-a (mam nadzieję). Normalnie, posmutniałem - już szykowałem się na narzekanie i marudzenie przy każdej okazji, a tutaj klops. Bywa...

W każdym razie, test zrobiony dla dwóch kości i trzech siatek przebiegł całkiem nieźle. Nie wiem, jak ten system sprawdzi się w praktyce, ale dla przykładu rodem z UGP wszystko poszło sprawnie. Jedyne, czym należałoby się zająć w przyszłości, to możliwość oteksturowania siatek - ale to robota raptem na 10 minut, dodanie glTexCoord2f, licznika i jednego parametru. Więcej zajmie poprawianie dokumentacji. Krótki przykład, jak to się robi (i bynajmniej nie chodzi mi o ciastka z kremem):

CBone Bones(2, 3, 4); // dwie kości, trzy siatki, cztery punkty na siatkę - mieszanka wybuchowa
//...

// ustawianie kości i wierzchołków - nudna pisanina, wiele argumentów, żadnej akcji - wypisz wymaluj praca urzędnika
//...
// w funkcji renderującej

Bones.ChangePosition(); // uaktualniamy pozycję kości (bo ktoś np. odstrzelił nogę)
Bones.RenderBonesVertices(); // rysowanie siatki z kośćmi

Bones.RenderBones(0.5); // jeśli trzeba, rysowanie samych kości w skali 0.5

//...

Oczywiście, do tego dochodzą funkcje ustawiające parametry kości, ich translacje i rotacje. Całość konstrukcji przypomina nieco klasy CTextureManager, CParticles czy CSoundManager (i ogólnie managery) - system RAII, czyli:

// jako pole prywatne klasy
int* elementy;

// konstruktor
Klasa::Klasa (int ilosc)

{

elementy = new int[ilosc];

}

// destruktor

Klasa::~Klasa()

{

delete[] elementy;

elementy = NULL;

}

W sumie, to stosowałem tą konstrukcję zanim jeszcze dowiedziałem się, co oznacza RAII. Wiadomo, że prochu nie wymyśliłem - lecz może ktoś na tym skorzysta.

Tak to mniej więcej wygląda. Dzisiaj dosyć mało spektakularnie - zrozumcie zmęczonego człowieka... Stanowczo radzę, szukajcie sobie jakieś roboty pomiędzy maturą a studiami.

Pozdrawiam i dziękuję - SceNtriC.

sobota, 6 września 2008

Rozwój frameworka trwa...

Uff, wreszcie uporałem się z tym, o czym pośrednio pisałem przy okazji poprzedniej notki. Zmuszony też jestem dodać siódmą kategorię błędów: "Program się kompiluje, uruchamia, działa, ale gdzieś jest jedna mała paskudna literówka i wszystko szlag trafia, łącznie z programistą". Co mam dokładnie na myśli - widać na screenie.

Uff (po raz drugi), w każdym razie dzisiaj kolejna notka z nowymi "ficzerkami" frameworka. Niedługo (choć pewnie nie dzisiaj) opublikuję kolejną partię dokumentacji na wikispacji i postaram się dokładniej przyglądnąć algorytmowi użytemu w tym, co właśnie się pojawiło. Wstyd się przyznać, ale w większości po prostu ślepo przepisałem kod z przykładu z Ultimate Game Programming i pewnie dlatego męczyłem się z błędem. No, ale nic - ważne, ze już działa. Zwłaszcza, ze to dosyć istotna partia frameworka. Jednakże napisze o tym na końcu notki.

Logger

Cóż, zdecydowałem się na napisanie małego loggera, który tak naprawdę miał mi pomagać przy wszelkiego rodzaju wyłapywaniu błędów. Kod tego mechanizmu jest wręcz diabelsko prosty i jakbym się uparł, to mógłbym go tutaj nawet zamieścić - ale sztuczne przedłużanie notki nie jest mile widziane wśród czytelników. Zwłaszcza, że do końca ten log tak wspaniale nie działa - po każdym wypisaniu daty, a przed właściwym tekstem przechodzi do następnej linijki - jest to jednak na tyle mało istotna sprawa, iż uznałem, że zajmę się tym później. Lub jeszcze później. Albo w ogóle.

A jak wygląda przykładowy log z mojego loggera? Proszę bardzo:

Sat May 20 15:21:51 2000
Do tej pory wszystko działa
Sat May 20 15:21:58 2000
A teraz nic nie działa - błąd krytyczny, heap corruption albo pieprzona jedynka napisana zamiast dwójki

A jak się tego używa:

CLogger Logger("log.txt"); // konstruktor z nazwą pliku

Logger.Write("Jakis tekst"); // tak, i to wszystko - żadnych fprintów i innych takich
// a program już sam zatroszczy się o otwarcie i zamknięcie pliku

SkyDome

Cóż, skoro mamy otwartą przestrzeń (tak, to jest ten ficzerek), to wypadałoby wyświetlić coś w rodzaju nieba. Mówisz - masz (i często płacisz, niestety). Fakt, że akurat w przykładzie z UGP był SkyDome, a nie SkyBox czy SkyPlane to zupełnie inna bajka. W takim czy innym razie, sklepienie niebieskie wygląda całkiem przyzwoicie i zadowalająco (widać to zresztą na screenie). Jest także całkiem szybkie i "niezniszczalne" - nie ma siły, aby gracz wyszedł poza obręb nieba, gdyż ono zwyczajnie porusza się wraz z kamerą. Dodatkowo, na dole jest tworzona płaszczyzna zamykająca cały obszar.

Przykład stworzenia nieba:

CSkyDome Sky; // gdzieś globalnie

Sky.Init(15.0f, 15.0f, 1024.0f); // zainicjowanie
Sky.LoadTexture("sky.tga"); // wczytanie tekstury nieba

Sky.RenderSkyDome(); // wyrenderowanie nieba

Akurat CSkyDome korzysta z klasy CTextureManager - w sumie mała profanacja, bo przyjmuje tylko jedną teksturę. A z kolei klasa opisana dwa nagłówki poniżej sam z siebie generuje teksturki.

Drzewo ósemkowe

Zagadnienie, które mnie elektryzowało od jakiegoś czasu, bo wiedziałem, że służy do optymalizacji działania przestrzeni 3D. Nie bardzo tylko miałem pojęcie jak to wygląda. Tak naprawdę dzisiaj też nie wiem wiele więcej, ale wystarczająco, aby uznać to za krok naprzód. "Octrees" są używane przez klasę CTerrain, ale równie dobrze mogą być użyte przez programistę w swoich własnych (czasami niecnych) celach. A na czym się ogólnie opiera zasada? Otóż główny węzeł (można go sobie wyobrazić jako taką kostkę) rekurencyjnie rozdziela się na osiem bocznych (z każdego wierzchołka jeden). Wszystko dalej jest rozszerzane dopóki cała przestrzeń nie znajdzie się w obrębie drzewa - gdyż tak nazywa się ta dziwna figura stylistyczno-techniczna. Poszczególne kostki zawierają odpowiednie partie wierzchołków i tak naprawdę (w zależności od ustawienia kamery) to one są wyświetlane, a nie fizyczny teren. Przy większości przypadków, daje to ogromnego kopa szybkościowego - aż wstyd, że tego sam nie napisałem. No, ale cóż, czasami trzeba...

Wczytywanie terenu z mapy wysokości i wyświetlanie go

Po jaki wiatrak niebo i dziwne kostki układające się w drzewko, którego nie zobaczymy w żadnym ogrodzie botanicznym? Ano, aby wyświetlić otwartą przestrzeń. Całkiem przyjemnej rzeczy doczekał się framework. Mapa wysokości to po prostu plik .raw z dwuwymiarową definicją terenu, który dopiero w programie jest odpowiednio odczytywany i generowany. Ogólnie, operacje na wierzchołkach (zwłaszcza tekstur), są w tej mechanice dosyć skomplikowane i zapewne dużo wody w Wełnie upłynie, zanim się zorientuję, co się z czym je.

Przykład:

CTerrain Terrain; // gdzieś globalnie

Terrain.LoadTerrain("map.raw", 8, true); // wczytanie mapy wysokości
Terrain.LoadTile(TT_LOWEST_TILE, "lowest.tga"); // wczytanie kafli
Terrain.LoadTile(TT_LOW_TILE, "low.tga");
Terrain.LoadTile(TT_HIGH_TILE, "high.tga");
Terrain.LoadTile(TT_HIGHEST_TILE, "highest.tga");
Terrain.LoadDetailTexture("detail.tga"); // wczytanie tekstury detali
Terrain.GenerateTextureMap(256); // wygenerowanie tekstury terenu

Terrain.RenderTerrain(); // render terenu

Dla porównania można spojrzeć tutaj - jest to właśnie przykład, na którym się bardzo mocno opierałem. Generalnie polecam tą stronę - i błogosławię autora za to, co tam umieszcza/umieścił.

Cóż, na tym kończę sobotnią notkę - pokój z Wami.

(A przedpokój z Tobą, cwaniaku...)

Pozdrawiam i dziękuję - SceNtriC.

środa, 3 września 2008

Krótki wykaz pułapek czekających na programistę

Błędy w kodzie zdarzają się każdemu. Skłonny nawet jestem stwierdzić, że najgłupsze błędy zdarzają się najczęściej najlepszym - wtedy automatycznie spada czujność na najprostsze zapisy, a że kompilator nie zawsze trafnie wyraża się o błędzie, to może człowieka szlag trafić. Oczywiście, tym wstępem nie mówię, że czuję się najlepszy - wprost przeciwnie. Ale błędy zdarzają mi się bardzo często - do tego dołóżcie moją nerwowość, gdy coś nie idzie zgodnie z planem i już macie wizerunek owłosionego człowieka łamiącego klawiaturę i rzucającego nią w monitor. A normalnie jestem siłą spokoju.

Ostatnio zresztą zrobiłem sobie małą przerwę przy Apokalipsie (muszę się zastanowić, co dalej z modelami) i zająłem się dalszą rozbudową mojego frameworka. Jako że nie chce już mi się siedzieć nad oknem windowsowym, shadery są dalej dla mnie niewiadomą (a przynajmniej na tą chwilę), to postanowiłem poszukać czegoś, czego jeszcze nie próbowałem. Oczywiście, podchody na niezawodnej stronie Ultimate Game Programming przyniosły oczekiwany skutek i dzięki temu od 3-4 dni siedzę nad błędami w kodzie. Ale jeśli się uda, to będzie kolejny ciekawy "ficzer" w frameworku.

Ale do rzeczy - analiza kodu przykładowego i jego "przepisanie" (z własnymi poprawkami i potrzebą automatyzacji np. renderingu) skłoniła mnie do refleksji nad rodzajami błędów, jakie mogą czekać na programistów.

1. Nieudana kompilacja - błędy składniowe

Szczerze mówiąc, jest to chyba najpiękniejszy błąd dla programisty. Tzw. "syntax error" często tyczy się braku średnika czy innych znaków, które nieopatrznie postawiliśmy w złych miejscach. I nawet, jeśli liczba ostrzeżeń tego rodzaju jest liczona w setkach, to poprawienie tego nie nastręcza wielkich trudności. Niestety - rzadko kiedy tylko takie bugi nam się przytrafiają...

2. Nieudana kompilacja - błędy dotyczące niewłaściwego używania elementów kodu

Te błędy już gorsze, choć nie sprawiają, że człowiek chce się rzucić z mostu. Najczęściej spotykane to nieodpowiednie odwołania do metod czy pól albo błędy rzutowania. O dziwo, te bugi wcale nie są trudne do zauważenia i zrozumienia. Problem może pojawić się w momencie, kiedy mamy naprawdę rozbudowany kod i jedna zmiana pociąga za sobą inne - tak chociażby bywa, gdy początkowo decydujemy się, żeby funkcja zwracała np. wartość typu float*, a potem jednak okazuje się, że nie ma zwracać i trzeba zmieniać i dostosowywać. A potem się okazuje z kolei, ze zwraca - ale nie kod, tylko właściciel, bo już mu się... Chce.

3. Nieudana kompilacja - nieudane linkowanie

Według mnie najgorszy typ błędów na etapie kompilacji. Kiedyś był dla mnie jeszcze straszniejszy, ale zweryfikowałem ten pogląd jak zacząłem regularnie spotykać się z "errorami" kolejnego rodzaju. Na czym zwykle polegają wywody linkera? Na dyrektywach include i tych dziwnych napisów z dwoma dwukropkami. Mówiąc jaśniej - należy uważać z umieszczaniem odwołań do odpowiednich plików nagłówkowych. Zwykle automatycznie umieszczałem je w pliku nagłówkowym klasy myśląc, iż plik źródłowy sam sobie pobierze odpowiednie headery. A tu g...uzik. W dodatku, często komunikaty linkera są bardzo zawiłe i nie zawsze trafiają w gusta kodera. Dlatego należy rozważnie operować makrami, a w ostateczności stosować coś takiego:

#ifndef _FILE_XYZ_H_

#include "xyz.h"
#define _FILE_XYZ_H_

#endif

Odpowiednio użyte powinno pomóc - dzięki temu unikniemy sytuacji, gdy klasy na szczycie "drzewa" są znowu dołączane w głównym pliku. U mnie w kodzie ten problem jest chociażby z klasą CTextureManager, bo wiele klas chciałoby mieć własny kod odpowiedzialny za teksturowanie.

A jakie mogą być inne błędy ujawniające się tutaj? Wprawdzie mogą one też być wywołane wcześniej, ale najczęściej tutaj okazuje się, że dla definicji jakiejś metody klasy nie umieściliśmy np. CKlasa::Metoda. Jednak te bugi są stosunkowo łatwe do wytropienia.

4. Udana kompilacja, ale są jakieś przekłamania w aplikacji

Najgorsze błędy zaczynają się tutaj, choć ten rodzaj nie musi być jeszcze tak uciążliwy. Bywa jednak, że znalezienie błędu zajmuje sporo czasu i kosztuje wiele neurytów. Często sa to po prostu przeoczenia, a w szczególnych przypadkach niepoprawne rozumowanie pewnych elementów programu. Mogę podać przykład na podstawie pisania Apokalipsu - przeoczeniem były chociażby źle podane współrzędne wierzchołków ścian czy chociażby różne dziwne rzeczy związane z przechodzeniem do menu i z powrotem, a niepoprawnym rozumowaniem popisałem się przy implementacji otwierania drzwi - troszkę to wtedy trwało, zanim doszedłem co jest źle.

5. Udana kompilacja, ale podczas działania programu są jakieś komunikaty - choć aplikacja działa

To już gorsze rzeczy, bo dotyczą najczęściej typowych rzeczy dla języka - albo niepoprawne użycia wskaźników albo tzw. "heap corruption" czyli przekraczanie indeksów tablicy połączone z niepoprawnym zwolnieniem zasobów. Tu już trzeba się wgłębić w kod, a ja na razie doszedłem tylko do jednej rzeczy, o której wiedza czasami się przydaje - nie zawsze nawet najbardziej poprawne operowanie pamięcią za pomocą new i delete jest przyjmowane bez błędów. Dlatego często liczę się z "nieprofesjonalizmem" i stosuję zwykłą deklarację zmiennych - tyczy się to zwłaszcza tablic dynamicznych, a najczęściej std::vector (nie wiem, może po prostu ja nie umiem tego używać).

6. Udana kompilacja, ale program się nie chce uruchomić

Najbardziej znienawidzony chyba komunikat Windowsa - i zawsze to durne pytanie czy wysyłać raport czy też nie. No cóż - na tym etapie już trzeba mocno się napracować, bo błąd może się znajdować dosłownie wszędzie - najczęściej chyba w takich rzeczach jak WinAPI czy pobieranie rozszerzeń OpenGL. Ostatnio miałem też problem z Apokalipsem w trybie release - jak się okazało, nie pasowało mu ciągłe kasowanie ostatnich danych o ścianach kolizyjnych i dołączanie ich na nowo, dla aktualnego stanu obiektów ruchomych w grze. Ale z kolei, gdy zamienił mechanikę kasowania/dołączania na nadpisywanie, kod działał już poprawnie. Niestety, przy WinAPI czy obecną partią frameworka nie mam już tyle szczęścia.

Tak mniej więcej się to przedstawia. Z pewnością coś pominąłem albo nie opisałem z należytą dokładnością. Myślę jednak, że tego typu artykuł może podziałać relaksująco na początkujących, którzy np. denerwują się niezrozumiałymi komunikatami kompilatora. W jaki sposób? Zawsze może być przecież gorzej...

Pozdrawiam i dziękuję - SceNtriC.