Stało się. WTF/LC kodu shaderów przekroczył krytyczny poziom i zabieram się za implementację innego modelu niż #ifdef-owy. Status shaderów: 900 linii kodu, 40 kilka parametrów, w tym ponad 16 różnych samplerów. Wyniki moich zmagań opiszę w stosownym czasie.
Jest kilka ogólnych podejść do zrobienia edytora do gry:
- dwie w całości niezależne aplikacje (każda z własnym rendererem itd.) -- na coś takiego zdecydowałem się przy PK, niestety takie podejście wymaga pisania dwukrotnie sporej części kodu, co na dłuższą metę jest bardzo nużące. Może jednak dobrze sprawdzić się w sporych zespołach, gdzie pewna grupa zajmuje się tylko edytorem -- wtedy obydwie grupy (czyli też ta od gry) wymieniają między sobą właściwie tylko informacje o formatach plików
- współdzielenie modułów -- edytor to osobna aplikacja, korzystająca jednak z tego samego renderera i innych elementów silnika. Mniej wspólnego kodu, ale jeżeli gra i edytor mają być w różnych językach (C++ i C#), to dochodzi nam sklejenie środowiska natywnego i CLR
- gra z wbudowanym edytorem -- edytor na stałe wbudowany w główny plik gry. Ekstremalnym przykładem są edytory z CryTEK (Far Cry, Crysis). Takie podejście wymaga jednak specyficznej architektury gry
- gra i edytor w osobnych aplikacjach/procesach, współpracujące ze sobą
Ostatnie podejście na swoim blogu opisał Maciej Sinilo (programista CDPR). Spróbowałem opisanego przez niego sposobu (aczkolwiek stosując gniazda zamiast potoków) i okazało się to strzałem w dziesiątkę.
Trzeba jednak pamiętać, że przy przesyłaniu komunikatów gra<-->edytor trzeba zmierzyć się z problemem architektonicznym. Komunikaty mogą bowiem być:
- przesyłane bezpośrednio do świata (stwórz obiekt X)
- wymagające użycia logiki gry (która musi z edytorem współpracować)
- zmuszające do wyświetlenia czegoś, albo przejścia w inny tryb (na przykład warto by w czasie edycji materiału wyświetlał się przykładowy obiekt pokryty tym materiałem)
- przesyłane w pewnym formacie (czysto tekstowy/XML/binarny), trzeba umówić się co z przesyłaniem danych (plików)
Generalnie problemów trochę jest, ale podejście wydaje mi się całkiem słuszne. Pewnie za kilka tygodni będę mógł opowiedzieć, jaki jest WTF/m socketowego edytora. ;)
PS. "Wnętrzności" edytora widać na załączonym screenie.

Ostatnio zastnawiałem się czy w przypadku silnika 3D model HSL nie sprawdziłby się lepiej niż RGB. Przecież to czysty zysk!
- realizacja wielu efektów graficznych w HSL jest o niebo prostsza. Zmiana kolorów (chociażby efekt sepii)?
- można zrealizować fajne systemy cząsteczkowe wykorzystujące HSL do różnicowania poszczególnych cząsteczek (np. małe odchylenia od barwy)
- a przede wszystkim, w HSL można bardzo tanio zrealizować HDR!
Warstwa barwy (Hue) nie potrzebuje dokładności większej niż 8 bitów. Warstwę nasycenia można zreduować z 256 do 32 kolorów bez zauważalnej straty jakości (5 bitów). Pozostałe 3 bity możemy przeznaczyć na kanał jasności, który w HDR ma kluczowe znaczenie. Możemy ewentualnie zrezygnować z kanału alfa (albo zmniejszyć jego dokładność) tym samym jeszcze zwiększając precyzję jasności.
Koszta przygotowań: niewielkie. Wszystkie tekstury trzeba przekonwertować z RGB do HSL (np. przed spakowaniem ich do VFS). Co z modelem oświetlenia? Nasz stary dobry NdotL zostaje bez zmian. Jedyna różnica jest we wpływie światła na barwę i nasycenie. Kwestia dobrania dobrego wzoru*. No i na koniec końcową scenę trzeba potraktować pixel shaderem, który przekonwertuje nam obraz z HSL do RGB. Nie jest on trywialny, fakt, ale dla dzisiejszych kart nie jest też jakimś problemem.
PS. Obrazek pochodzi z gry GRID. Od kilku dni gram w demo i mam ochotę kupić nie tylko pełną wersję, ale i jakąś wypasioną kierownicę do komputera. :P
* PS2. OK, magic works here. Trzeba by się nad tym zastanowić. Ale ponownie, wydaje mi się, że daje to większe możliwości niż banalne materialRGB * lightRGB.
Dołączyłem do projektu Airsoft Combat Ultimate. Jest to multiplayerowy symulator airsoftu. Mam nadzieję, że współpraca okaże się owocna tak dla projektu, jak i dla mojego silnika. :)
Jako bonus dwa obrazki. Jeden to render z Blendera, drugi to obrazek z silnika. Zgadnijcie, który jest który.
Budujemy silnik: architektura, renderer
20.12.2008 21:14 in grafika 3D, cg, OpenGL, programowanie, C++, silnik
Mój silnik składa się z kilku modułów. Każdy moduł to osobny plik DLL (tutaj wielkie brawa dla VS, które radzi sobie z solution z kilkoma projektami DLL i dwoma projektami gier tak, jakby wszystko było w tym samym projekcie -- powiem szczerze, że nie spodziewałem się, że w C++ coś takigo się uda). Modułem jest na przykład obsługa danych, wejścia, dźwięku, grafiki, fizyki, etc. Przykładowy kod gry wygląda następująco:
void uGame::Run()
{
try
{
uDataManager dm("data.utd");
uWorld world;
uRenderer renderer(dm, world);
renderer.Init();
uLight *l = new uLight();
l->SetPosition(...);
...
world.AddObject(l);
uModel *m = new uModel();
m->SetFilename("car.utm");
world.AddObject(m);
uCamera *c = new uCamera;
c->SetPosition(...);
...
world.AddObject(c);
renderer.SetCamera(c);
world.OnLogic += UDELEGATE(&MyLogic);
renderer.StartLoop();
}
catch (uException e)
{
...
}
}
void uGame::MyLogic(uWorld &world)
{
...
}
Jak widać, użycie renderera zostało sprowadzone do trzech bardzo prostych operacji. Ważne rzeczy:
- renderer nie jest bezpośrednio sterowany przez użytkownika (w tej wersji ma pomocniczą funkcję SetCamera, która zostanie jednak zastąpiona mechanizmem ustawień)
- wszystkie opcje (rozdzielczość, detale czy w przyszłości aktywna kamera) są przekazywane jako obiekty typu RendererSettings (dziedziczącym po abstrakcyjnym obiekcie, tak samo jak kamera czy model)
- renderer sam ,,wpina'' się w świat, dodając swojego delegata do obsługi zdarzenia AddObject
- przy okazji: prosty kod do obsługi delegatów w stylu C#:
#define UDELEGATE(a) fastdelegate::MakeDelegate(this, a);
template <class T>
class uEvent1
{
typedef fastdelegate::FastDelegate1<T> _delegate;
std::list<_delegate> mDelegates;
public:
void operator +=(_delegate &d)
{
mDelegates.push_back(d);
}
void operator -=(_delegate &d)
{
mDelegates.remove(d);
}
void clear()
{
mDelegates.clear();
}
void operator() (T &value)
{
for (std::list<_delegate>::iterator
it = mDelegates.begin(); it != mDelegates.end(); it++)
{
(*it)(value);
}
}
};
- jak było wspomniane wcześniej, użytkownik nie ,,obsługuje'' samemu renderera. To renderer, korzystając z modułu danych, wczytuje niezbędne modele, tekstury i inne zasoby
Takie podejście to pełne oddzielenie wyświetlania danych od ich logicznego znaczenia. Pozwala na równoległą pracę nad rendererem i innymi modułami w przyjemny i bezpieczny sposób. Umożliwia kompletną wymianę renderera. Na koniec można zauważyć, że w ten sposób w silniku 3D realizujemy wzorzec MVP (model-widok-kontroler). :)
W C# 3.0 pojawiła się możliwość rozszerzania klas własnymi metodami (extensions methods). Poza tym, że na tym mechanizmie opiera się LINQ, można wykorzystać go dla własnej wygody:
static class StringExtension
{
public static int LetterCount(this string s, char c)
{
int r = 0;
for (int i = 0; i < s.Length; i++)
{
if (s[i] == c) r++;
}
return r;
}
}
static class Program
{
public static void Main()
{
Console.WriteLine(
"Suprisingly evil cat".LetterCount('a').ToString());
}
}
Na tym, można by w zasadzie zakończyć opis tego mechanizmu, ale... No właśnie. Jak zauważył mój kolega z wydziału, klasy StringExtension nigdzie (poprzez swoją nazwę) nie używamy (choć oczywiście możemy). Brak możliwości anonimowego rozszerzenia wyżej wspomniany kolega skwitował tak:
/*
* Statyczna klasa potrzebna do rozszerzenia clasy Delegate o dodatkowe metody
*/
static class bardzoSmisznaKlasaKtoraJestChybaBugiemProjektowymCsharpaasdfasdfasdfFSasdfasdfasfasfafasfafsDFASDFASDFASFGASFASDFASDFASDFASFASDFITakTejNazwySieNigdyNieUzywa
{
Przyłączam się do prośby o nadanie koledze certyfikatu MSCP. ;)
Budujemy silnik: organizacja shaderów
18.12.2008 20:45 in grafika 3D, cg, OpenGL, programowanie, silnik
Jak możemy zarządzać kodem shaderów w silniku? Podstawowe podejścia są trzy:
- przygotować stały zestaw shaderów i dać użytkownikowi jedynie możliwość wyboru spośród nich. Podejście to sprawdzi się, niestety, jedynie w bardzo prostych scenach.
- metoda addytywna - przez niektórych uznawana za Święty Graal. Polega na ,,rysowaniu'' (a przynajmniej układaniu) shadera w sposób graficzny (przykład modelu addytywnego). Szczerze powiedziawszy do mnie zupełnie ten sposób nie trafia. Bo kto niby miałby te shadery układać? Grafik? Bez sensu, nawet jeżeli radziłby sobie z ich edycją (rzecz nietrywialna), to jak miałby uwzględniać różne parametry sceny? Map designer? Cieplej, ale czy map designer musi być inżynierem ds. shaderów? Ostatecznie może to robić programista -- tylko po co, skoro w żaden sposób nie ułatwia mu to zadania?
- wreszcie mamy metodę substraktywną - polegającą na napisaniu (w uproszczeniu) jednego wielkiego shadera, z którego będziemy wybierali potrzebne części. Skupię się właśnie na niej.
Z tą metodą spotkałem się po raz pierwszy w silniku The Final Quest Adama Sawickiego. Ideę rozwinąłem do następującej postaci:
- uwaga formalna: korzystam z Cg
- wszystkie parametry są deklarowane na początku pliku shadera
- kluczowy rodzaj materiału jest zdefiniowany jako osobna procedura (entry point). Przykładowo, w moim silniku osobnymi procedurami są shadery simple (diffuse+specular), metallic (environmental mapping), bump (normal/parallax mapping), water
- wszystkie inne parametry zdefiniowane są jako kombinacje makr
- możliwie spójne bloki funkcjonalne są definiowane jako funkcje wykorzystujące zmienne ,,globalne'' - przykład: liczenie zaniku światła:
Budujemy silnik: filozofia
15.12.2008 23:59 in grafika 3D, cg, OpenGL, programowanie, silnik
Na forum gamedev.pl znalazłem następujący wpis:
Naszły mnie wątpliwości co do sensu silnikow graficznych jako dużych, zaawansowanych projektów. Wiec, chodzi o to, że w dobie fixed-pipeline wygląd gry, efekty jakie w niej występowały, zależaly tylko i wyłącznie od programistów. Oni to, pisali w całości w kodzie wszystko to co mialo zostać wyrenderowane. Silnik wydawał się sensowną częścią gry, zawierał w sobie cały kod wszystkich fajnych rzeczy do których narysowania był zdolny i życie wydawało się proste. Jednak pojawiły się shadery, effect'y i wszystko co kiedyś musieli zawierać programiści w engine, tworzą map designerzy czy modelerzy.
Stąd moje pytanie - czy wartość silnika nie zmalała przez przypadek, jako następstwo programowalnych GPU, do frameworka którego rola sprowadza się do szybkiego renderowania tych wszystkich shaderow?
Wiele wody w wirtualnych rzekach upłynęło od czasu napisania tego posta, ale postaram się tutaj odpowiedzieć na to pytanie.
Otóż nie, rola silnika nie zmalała z powodu odejścia od fixed-pipeline. Powiem więcej: dopiero teraz jego rola stała się dominująca! Pomijam tap pisania software'owych rendererów, z których każdy miał siłą rzeczy inne możliwości (ale kiepskie możliwości sprzętu skutkowały kiepskimi -- tak czy inaczej -- efektami). W czasach świetności fixed pipeline każdy silnik stanowił bowiem z grubsza nakładkę na zwykły OpenGL-owy potok (jeżeli nie lubisz OGL, zamieniaj wszystkie jego wystąpienia na DX). Możliwości były wszakże mocno ograniczone. Jeżeli mgła, to raczej standardowa (plus jej konfiguracja). Światła? OK, ale poza podaniem kilku kolorów i pozycji światła nie dało się zbytnio zcustomizować.
Teraz mamy potężne narzędzie: shadery. Ale czy shader jest równoważny końcowej grafice? W prostych zastosowaniach (demonstracja efektu graficznego) generalnie tak. Ale w przypadku większych projektów (gier) użyty shader to tylko mały krok na drodze do wielkości. Dlaczego?
Po pierwsze, każdy shader jest -- mimo wszystko -- dosyć podobny. W dużym uproszczeniu możemy powiedzieć, że to takie NdotL z dodatkami. ;) Oczywiście te ,,dodatki'' to istota zagadnienia, ale sami przyznacie, że ,,zwykły'' (,,prosty'') shader do każdego silnika/renderera będzie podobny?
Po drugie, shader to nie tylko kod. To również (a może nawet przede wszystkim) pewne dane wejściowe. To my musimy zatroszczyć się o przekazanie odpowiednich macierzy, pozycji kamery i światła czy innych niezbędnych do obliczeń elementów. Zaimportowanie do silnika shadera wymagającego obliczeń w przestrzeni stycznej (tangentów) nie jest trywialne, jeżeli nie przewidzieliśmy takiej możliwości przy budowie modeli. Podobnie, ciężko nam będzie skorzystać z cubemapy (zwłaszcza dynamicznej), jeżeli nasza architektura tego nie uwzględniła.
Po trzecie, liczba kombinacji faz renderowania jest przeogromna. Każdy obiekt może być renderowany:
- różnym rodzajem materiału: prostym, metalicznym (mapowanie środowiskowe), przezroczystym (lustra), ...
- dla różnych świateł: kierunkowe, punktowe, spotlighty
- uwzględniając zacienienie lub nie (plus różne techniki SM/SV)
- z innym przetwarzaniem wierzchołków: prostym, z animacją na kościach, z displacement mappingiem, ...
- w innym kontekście: wypełnianie Z bufora, rysowanie geometrii, rysowanie map cieni, rysowanie odbić, cubemap, testowanie przesłaniania
Jak widać, ilość tych kombinacji jest nietrywialna od ogarnięcia ze strony silnika. A do tego trzeba ów silnik napisać tak, by nasza gra działała z przyzwoitą wydajnością. To niełatwe zadanie, ale kto powiedział, że bycie programistą silnika jest proste? ;)
Temat budowy silnika będzie kontynuowany. Następna notka: organizacja shaderów (mam nadzieję, że wkrótce).
Tym razem nietypowo, bo nieprogramistycznie, ale sytuacja tego wymaga. Drodzy Czytelnicy! Kupujcie ,,Fakt'', bo tylko w nim znajdziecie potężny trójkąt na układ moczowy! Zobaczmy, co redaktorzy owego dziennika piszą na temat swojej najnowszej medyczne rewelacji:
Czy trójkąty zadziałają? Czytelnicy nie mają wątpliwości! Trójbarwny pas, który ukazał się w Fakcie, pomógł wielu na dolegliwości kręgosłupa. Białe serce przyniosło ulgę zmagającym się ze złym ciśnieniem, niedokrwieniem i arytmią serca. Elipsa na układ oddechowy pomogła zwalczyć kaszel i uporać się z zadyszką, a złociste koło pomogło przy dolegliwościach układu pokarmowego. Trójkąty także będą pomocne.
Ja z niecierpliwością czekam na pentagram na problemy polityczno-społeczne i duocykloidę na sraczkę.
Niedawno minął rok odkąd założyłem bloga. Rok prowadzenia bloga to całkiem niezły wynik, chociaż muszę przyznać, że jest wiele rzeczy, które chciałbym poprawić. Oto moje obserwacje.
Styl pisania postów. Blogi dzielą się IMHO na dwie kategorie: pisane dla znajomych i te pisane dla ogółu. Tymi pierwszymi zajmować się nie będę. W przypadku bloga, który czyta przypadkowa osoba ważne jest kilka kwestii. Sprawa pierwsza: treść. Z jakiego powodu ktoś znalazł się na naszym blogu? Może wszechmocny Google podpowiedział mu, że znajdzie odpowiedź na swoje problemy, a być może trafił tu przez serię odnośników. Jeżeli nie znajdzie niczego, o czym chciałby poczytać, to sobie pójdzie. Jednak nie tylko tematyka jest ważna. Istotna jest też forma przekazania treści. U siebie -- niestety -- zauważam irytujący zwyczaj pisania w stylu wstęp-rozwinięcie-podsumowanie. Irytujący, bo większość czytających wie z grubsza o czym czyta i zaczynanie tekstu od W gospodarstwie domowym często stosowane jest urządzenie zwane potocznie komputerem... wcale nie zwiększa komunikatywności.
Co mam zamiar poprawić w przyszłości, jeżeli chodzi o blogowanie?
- komunikatywność (styl pisania zaprezentowany wyżej)
- częstotliwość i ilość zmian (chociaż często po prostu brakuje mi czasu, a mam mnóstwo pomysłów na nowe notki; niektóre nawet w formie szkiców)
- rozbudowanie ,,stałych działów'' (chciałbym rozszerzyć część z pseudo-portfolio)
- dodanie działów na temat moich hobby (w końcu nie jestem tylko nudnym programistą, a gitara i gry bitewne byłyby dużym urozmaiceniem bloga)
Dzisiaj zaczynam zmiany od dodania panelu ,,nad czym pracuję'' -- swoistego minibloga. Do dzieła!