Environmental mapping + skybox

Tym razem będzie mały update dotyczący silnika. Niby nic, ale można się pochwalić.

etuymcrypPo pierwsze Environmental mapping, czyli proste przybliżenie zjawiska odbicia światła na powierzchni ciała oraz załamania światła na granicy dwóch ośrodków. Wymaga dostarczenia tylko dodatkowej tekstury sześciennej (cube-mapa) oraz skorzystania z dwóch dodatkowych funkcji HLSL: refract i reflect. Można go wzbogacić dodatkowo o efekt Fresnela i rozszczepienie chromatyczne. Całość wygląda jak na screenie obok. Efekt ten nie jest trudny do zaimplementowania, więc szczegółów implementacyjnych nie będzie, zresztą są one w książce CG Tutorial.

Kolejną rzeczą jest skybox. Jak nazwa wskazuje, jest to pudełko z tłem nieba, które powoduje, że scena z obiektami nie wygląda po prostu pusto. Poniżej przedstawiam trzy kroki rysowania skyboxa:

  1. Potrzebne jest pudełko wraz z teksturą, którą jest już wspomniana wcześniej cube-mapa. Biblioteka D3DX posiada do tego odpowiednią funkcję: D3DXCreateBox(), która jako parametry przyjmuje wymiary pudełka. Najlepiej jest stworzyć sześcian 1:1:1.
  2. Pudełko powinno obracać się razem z kamerą, ale nie może zmieniać swojej pozycji. Aby to osiągnąć należy użyć tylko macierzy widoku i projekcji. przy czym ostatni wiersz tej pierwszej powinien mieć takie wartości: (0, 0, 0, 1).
  3. Skybox powinien być pierwszym elementem rysowanym na scenie, a dodatkowym warunkiem koniecznym by był on tłem całej sceny jest wyłączenie zapisu do ZBuffera. Dzięki temu każdy kolejny obiekt sceny będzie rysowany zawsze przed skyboxem. Aby odczytać kolor tekstury zamiast funkcji tex2D należy wykorzystać funkcję texCUBE, do której jako współrzędne tekstury przekazuje się pozycję danego pixela.

Jak widać nie jest to skomplikowane, a dzięki temu scena wygląda o wiele lepiej.

SDKMeshToXConverter

Już trochę czasu temu postanowiłem napisać konwerter wspomnianego formatu .sdkmesh do .x i w końcu dopiąłem swego. Mimo tego, że jak już wcześniej wspomniałem, format ten nie nadaje się do modeli animowanych (posiada informacje o animacji, ale nie ma informacji o kościach), można ostatecznie wyciągnąć z niego model statyczny.

To właśnie robi poniższa aplikacja. Wystarczy tylko przeciągnąć plik .sdkmesh na plik aplikacji i w katalogu mesha źródłowego pojawia się wygenerowany plik w formacie .x.

Pobierz

Kilka zmian

rpotStwierdziłem, że wypadałoby się wreszcie odezwać po kolejnej długiej nieobecności. W końcu udało mi się zaimplementować Skinning w silniku, oczywiście bazując na przykładzie SkinnedMesh z DX SDK. W praktyce zrobiłem  to co było w ów sample’u, a jedyną różnicą jest miejsce położenia kości, które u mnie znajdują się w hierarchii zaraz za modelem. Samą hierarchię tworzą modele (zwykłe drzewo, które ustanawia hierarchię na podstawie której tworzone są wynikowe macierze świata dla tych modeli). Póki co mam tylko dwa rodzaje modeli: StaticMesh i SkinnedMesh, ale bez problemu można do niej wstawić innego rodzaju obiekty, które występują na scenie.

Kolejną sprawą są stosowane przeze mnie formaty. Otóż w poprzedniej notce napisałem jakoby format .x był bardzo prostym formatem. Nic bardziej mylnego, jest on bardziej złożony niż myślałem (chodzi mi tylko o format modeli, nie o system zapisu danych). W standardowym pliku .x może znajdować się więcej modeli, które są ułożone w hierarchię, a do ich załadowania służy funkcja:

D3DXLoadMeshHierarchyFromX();

Funkcja ta ładuje nie tyko wspomnianą hierarchię meshy, ale również informacje o kościach i  animacji. Niestety format materiałów dalej nie zawiera nic więcej poza strukturą D3DMATERIAL9 i nazwą tekstury diffuse.

Natomiast format SDKMesh jest jeszcze gorszy (mimo jednej zalety, że posiada nazwy dla tekstur normal i specular), ponieważ nie posiada on żadnych informacji o kościach mimo, że są informacje o animacji. Po prostu wczytywany mesh jest już przekonwertowany i gotowy do wyświetlenia (co blokuje możliwość prostego użycia ID3DXSkinInfo, którym mógłbym software’owo przekształcić model np. do kolizji per vertex).

Shaderki

AplikacjaPostanowiłem wreszcie zająć się wyświetlaniem grafiki opartej na shaderach. Stworzyłem sobie prosty framework oparty na moim „silniku”, który jest aktualnie tylko szkieletem aplikacji. Framework składa się z kilku klas, których zadaniem jest uprościć wykonywanie różnych rzeczy. Aktualnie w zestawie jest kamerka FPP, prosty system cząsteczek oraz klasy do wczytywania modeli i zarządzania efektami.

Na dole tej notki znajduje się link do aplikacji, która reprezentuje aktualny stan kodu. Aplikacja wyświetla 4 modele wczytane z plików w formacie .x, 1 model z formatu .sdkmesh oraz cząsteczki, których pozycja obliczana jest w shaderze. Światło użyte w scenie jest punktowe, a obliczenia są wykonywane w pixel shaderze. Materiały modeli pochodzą z ich plików, dlatego nie wszystko wygląda super ;).

Kamerkę obsługuje się za pomocą myszy i klawiszy WSAD, a światło za pomocą strzałek oraz klawiszy PG_UP i PG_DN.

Pobierz

Defekt optymalizacji kompilatora x64

Podczas dalszego kodowania mojego silnika natrafiłem na bardzo dziwny błąd kompilatora Visual Studio. Zaczynając od początku – mam oto taki kod:

enum
{
	// Rozmiary pól
	MAX_BITS_INDEX = sizeof(Size_t) * 4,
	MAX_BITS_MAGIC = sizeof(Size_t) * 4,
	// Maksymalne wartości pól (Przesunięcie 1 w dwóch operacjach, ponieważ inaczej kompilator nie daje rady)
	MAX_INDEX = ((1 << MAX_BITS_INDEX / 2) << MAX_BITS_INDEX / 2) - 1,
	MAX_MAGIC = ((1 << MAX_BITS_MAGIC / 2) << MAX_BITS_MAGIC / 2) - 1
};
/* ... */
 
// handle = 0x0000000400000006;   - przykladowo
// Size_t ma rozmiar wskaźnika (jest zależny od platformy)
Size_t value = handle & MAX_MAGIC;

Otóż problem z tym kodem jest taki, że gdy zostanie skompilowany na platformę x64, zostaje wygenerowany następujący kod w assemblerze:

    79:                 {
    80:                         Size_t value = handle & MAX_MAGIC;
 
000000013FC96F94  mov         rax,qword ptr [handle]
000000013FC96F9C  mov         qword ptr [value],rax
 
    81:                         if(value == *it) return true; // Dalszy kod

Jak widać problem polega na braku kluczowej instrukcji and, której zadaniem jest odcięcie najstarszych 32-bitów. Dla porównania kod assemblera wygenerowanego dla platformy x86 jest następujący:

    79:                 {
    80:                         Size_t value = handle & MAX_MAGIC;
00DC9DDC  mov         eax,dword ptr [handle]
00DC9DDF  and          eax,0FFFFh
00DC9DE4  mov         dword ptr [value],eax
    81:                         if(value == *it) return true; // Dalszy kod

Tutaj instrukcja ta występuje.

Błąd ten jednak nie występuje gdy samemu sprecyzuję stałą, tj.:

Size_t value = handle & 0xffffffff;

W tym przypadku zostanie wygenerowany poprawny kod.

Wniosek jest taki, że kompilator widząc operację and zmiennej ze stałą typu enum (który przechowuje wartości 32-bitowe), traktuje tą zmienną jako typ 32-bitowy. W tym przypadku stwierdza, że instrukcja and nie ma sensu, bo wartość MAX_MAGIC zawiera maksymalną wartość (0xffffffff). Według mnie jest to ewidentny błąd i nie powinno takie coś wystąpić.

// Edit :)
Problemem oczywiście jest to, że enum jest tak naprawdę typem int, więc się dokładnie tak zachowuje.

Systemy operacyjne

Już od jakiegoś czasu zanoszę się z wrzuceniem programów, które musiałem przygotowywać na zajęcia laboratoryjne z Systemów operacyjnych. Oczywiście wiadomo że „chęci największe, gdy możliwości najmniejsze”, więc wrzucam je dopiero teraz, gdy mam dostęp do komputera. Wszystko przez firmę ASUS, która zapewniła mi odwyk od komputera dzięki długoterminowej naprawie notebooka (niech będą przeklęci! :P).

Wracając do tematu, programy które należało przygotowywać miały symulować działania algorytmów takich jak:
- planowanie dostępu do procesora
- planowanie dostępu do dysku twardego
- pamięć wirtualna

Programy te nie są zbyt wyszukane, ale spełniają założenia. Oczywiście na przekór temu, czego używamy na wydziale (Java), swoje programy pisałem w C++. Programy są „prawie” multiplatformowe. Aby ruszyły pod linuksem wystarczy zamienić kilka funkcji i nagłówków na ich odpowiedniki.

Zadanie 1
Zadanie 2
Zadanie 3
Zadanie 4

AutomaticHandle ciąg dalszy

Jak się okazuje nic nie jest na początku idealne. Dzięki uwagom Revo udało mi się usprawnić szablon automatycznych uchwytów. Revo poradził mi dodanie dodatkowego wskaźnika na licznik odwołań do danego zasobu. Dzięki temu możliwe było usunięcie jednej funkcji wirtualnej, a druga została sprowadzona do roli sprzątaczki, która zostaje wywołana tylko w momencie, gdy licznik osiąga wartość 0. Dodatkowo dodałem sprawdzenie czy uchwyt nie jest zerowy przed odwołaniem się do wskaźników.

Poniżej znajduję się kod poprawionej klasy szablonowej AutomaticHandle:

typedef unsigned int Dword;
 
template< typename TAG>
class AutomaticHandle
{
public:
	// Podstawowy konstruktor
	AutomaticHandle(NLib::Dword handle, MHandleMgr* manager, NLib::Dword* resCounter) : m_handle(handle), m_manager(manager), m_resCounter(resCounter)
	{ if(m_handle) ++(*m_resCounter); }
	// Konstruktor kopiujący
	AutomaticHandle(const AutomaticHandle& src) : m_manager(src.m_manager), m_handle(src.m_handle), m_resCounter(src.m_resCounter)
	{ if(m_handle) ++(*m_resCounter); }
	// Destruktor
	~AutomaticHandle()
	{
		if(m_handle)
		{ if(!(--(*m_resCounter))) m_manager->ReleaseResource(m_handle); }
	}
	// Operator przypisania
	const AutomaticHandle& operator =(const AutomaticHandle& src)
	{
		if(m_handle)
		{ if(!(--(*m_resCounter))) m_manager->ReleaseResource(m_handle); }
		m_handle = src.m_handle;
		if(m_handle) ++(*(m_resCounter = src.m_resCounter));
		return src;
	}
	// Operator równości
	bool operator ==(const AutomaticHandle& src)
	{ return m_handle == src.m_handle; }
	// Operator różności
	bool operator !=(const AutomaticHandle& src)
	{ return m_handle != src.m_handle; }
	// Operator Dword
	operator NLib::Dword() const
	{ return m_handle; }
	// Operator bool
	operator bool() const
	{ return !!m_handle; }
 
private:
	// Uchwyt
	NLib::Dword m_handle;
	// Wskaźnik do managera
	MHandleMgr* m_manager;
	// Wskaźnik do licznika zasobu
	NLib::Dword* m_resCounter;
};

Kolejną klasą jest AutoHandleMgr, czyli klasa implementująca obsługę automatycznych uchwytów. Aktualnie tylko trzy funkcje są dostępne klasie pochodnej, w tym jedna dla uchwytów. Klasa ta posiada jedną funkcję abstrakcyjną, w której po zdefiniowaniu w klasie pochodnej należy usuwać odpowiednie zasoby zgodnie z przekazanym indeksem.

Oto jej definicja:

typedef unsigned int Dword;
 
class AutoHandleMgr
{
	// Unia do wyciągania informacji z uchwytu
	enum
	{
		// Rozmiary pól
		MAX_BITS_INDEX = sizeof(NLib::Dword) * 4,
		MAX_BITS_MAGIC = sizeof(NLib::Dword) * 4,
		// Maksymalne wartości pól
		MAX_INDEX = (1 << MAX_BITS_INDEX) - 1,
		MAX_MAGIC = (1 << MAX_BITS_MAGIC) - 1
	};
 
public:
	// Funkcja do zwracania uchwytu
	void ReleaseResource(NLib::Dword handle)
	{
		NAssert(GetIndex(handle) < m_magicValues.size(), "Wrong Handle");
		NAssert(CheckMagic(handle), "Wrong handle");
		DeleteHandleAt(GetIndex(handle));
		ReleaseResourceByIndex(GetIndex(handle));
	}
 
protected:
	// Funkcja tworzy nowy uchwyt i go zwraca
	// Index zawarty w uchwycie odpowiada indeksowi w tablicy z danymi
	// w odziedziczonej klasie
	NLib::Dword CreateNewHandle()
	{
		NLib::Dword handle;
		static NLib::Dword s_autoMagic = 0;
		if(++s_autoMagic > MAX_MAGIC) s_autoMagic = 1;	// 0 oznacza pusty uchwyt
		// Jeżeli nie ma wolnych miejsc to tworze nowy index
		if(m_freeSlots.empty())
		{
			handle = m_magicValues.size() << (MAX_BITS_INDEX - 1);
			m_magicValues.push_back(s_autoMagic);
		}
		else
		{
			handle = m_freeSlots.back() << (MAX_BITS_INDEX - 1);
			m_magicValues[m_freeSlots.back()] = s_autoMagic;
			m_freeSlots.pop_back();
		}
		handle |= s_autoMagic;
		return handle;
	}
	// Funkcja zwraca index
	NLib::Dword GetIndex(NLib::Dword handle)
	{ return (handle >> (MAX_BITS_INDEX - 1)) & MAX_INDEX; }
 
private:
	// Funkcja kasuje wskazany uchwyt
	void DeleteHandleAt(NLib::Dword index)
	{	m_freeSlots.push_back(index);
		m_magicValues[index] = 0; }
	// Funkcja zwraca część magiczną
	NLib::Dword GetMagic(NLib::Dword handle)
	{ return handle & MAX_MAGIC; }
	// Funkcja sprawdzająca wartośc magic
	bool CheckMagic(NLib::Dword handle)
	{	NAssert(GetIndex(handle) < m_magicValues.size(), "Wrong Handle");
		return GetMagic(handle) == m_magicValues[GetIndex(handle)]; }
 
private:
	// Funkcja dekrementująca licznik w zasobach
	virtual void ReleaseResourceByIndex(NLib::Dword index) = 0;
 
private:
	// Vector zawierający magiczne wartości
	std::vector m_magicValues;
	// Vector zawierający indeksy z wolnymi miejscami
	std::vector m_freeSlots;
};

W przypadku tego menadżera uchwyty muszą być usunięte przed nim, inaczej pojawią się błędy z dostępem do pamięci.

Refactor silnika + automatyczne uchwyty

Właśnie mija tydzień od zakończenia dwutygodniowego pobytu w domu w czasie końcówki sesji i przerwy egzaminacyjnej. W tym czasie rozpocząłem przepisywania mojego „silnika” od nowa, tym razem w oparciu o interfejsy. Są one bardzo poręczne, gdy chce się ukryć implementację klas wewnątrz statycznych, bądź dynamicznych bibliotek. Głównym założeniem przy projektowaniu drugiej wersji NIne było właśnie ukrycie implementacji wewnątrz statycznej biblioteki .lib. Dzięki takiemu postępowaniu biblioteka wygląda ładnie i pozwala ładnie rozszerzać własną funkcjonalność.

Drugim zagadnieniem poruszanym w tej notce są uchwyty, które same zarządzają licznikiem danego zasobu.
Z początku implementację uchwytów opierałem na artykule znajdującym się w książce „Game Programming Gems”, jednak problemem okazało się zmuszenie tychże uchwytów do sprzątania po sobie zasobów. Na szczęście rozwiązanie okazało się proste, ponieważ wystarczyło umieścić w klasie uchwytu wskaźnik do menadżera zasobów.

Poniżej znajduje się moja implementacja takiego uchwytu:

template< typename TAG>
class AutomaticHandle
{
public:
	// Podstawowy konstruktor
	AutomaticHandle(Dword handle, MHandleMgr* manager) : m_handle(handle), m_manager(manager)
	{}
	// Konstruktor kopiujący
	AutomaticHandle(const AutomaticHandle& src) : m_manager(src.m_manager), m_handle(src.m_handle)
	{ m_manager->Copy(src.m_handle); }
	// Destruktor
	~AutomaticHandle()
	{ m_manager->Release(m_handle); }
	// Operator przypisania
	const AutomaticHandle& operator =(const ManagedHandle& src)
	{ m_manager->Release(m_handle);
	  m_handle = src.m_handle;
	  m_manager->Copy(src.m_handle);
	  return src; }
	// Operator Dword
	operator Dword() const
	{ return m_handle; }
	/*
	...
	*/
 
private:
	// Uchwyt
	Dword m_handle;
	// Wskaźnik do managera
	MHandleMgr* m_manager;
};

Jak widać klasa zawiera dodatkową zmienną – wskaźnik na klasę menadżera ustawiany przy konstrukcji. Klasa menadżera musi zawierać używane metody, które wewnątrz zarządzają licznikiem odwołań na dany zasób, a w momencie jego spadku do 0, likwidować dany zasób.

Interfejs IMesh, klasa Mesh i loadery.

Nawiązując do poprzedniej notki chcę tylko zamieścić jak wyglądają klasy, które napisałem i czemu są takie fajne:

// Interfejs
class IMesh
{
public:
	/*
	...
	*/
	// Funkcja aktualizująca mesha
	virtual void SetMesh(LPD3DXMESH mesh) = 0;
	// Funkcja dodająca materiał do wektora
	virtual void AddMaterial(const Material&amp; mat) = 0;
	// Funkcja zwraca materiał o podanym indeksie
	virtual Material GetMaterial(Dword index) = 0;
	/*
	...
	*/
};
 
// Główna klasa Mesh
class Mesh : public IMesh
{
public:
	// Konstruktor
	Mesh();
	// Destruktor
	~Mesh();
 
public:
	/* Funkcje używane do rysowania modelu
	...
	*/
 
protected:
	/*
	...
	*/
	// Funkcja aktualizująca mesha
	virtual void SetMesh(LPD3DXMESH mesh) = 0;
	// Funkcja dodająca materiał do wektora
	virtual void AddMaterial(const Material&amp; mat) = 0;
	// Funkcja zwraca materiał o podanym indeksie
	virtual Material GetMaterial(Dword index) = 0;
	/*
	...
	*/
};
 
// Przykład loadera
class X_file
{
public:
	// Konstruktor
	X_file(IMesh* mesh) { m_mesh = mesh; }
	// Funkcja ładująca
	RESULT LoadMesh(String filename)
	{
		LPD3DXMESH mesh;
		/*
		... cuda-niewidy...
		*/
		m_mesh->AddMesh(mesh); // Wywołanie funkcji interfejsu
	}
 
private:
	// Obiekt na którym działam
	IMesh* m_mesh;
};

Nie lubię się rozpisywać. Jak można zauważyć dzięki interfejsowi IMesh można załadować dowolny model do klasy Mesh, ponieważ są publiczne wszystkie funkcje umożliwiające to zadanie, natomiast klasa Mesh posiada publiczne funkcje służące tylko i wyłącznie do rysowania modelu. Dzięki takiemu rozwiązaniu kod jest przejrzysty.

Programowania ciąg dalszy

Minęło trochę czasu od mojej ostatniej notki, ale co poradzić, studia są trochę wymagające, więc trzeba się czasem pouczyć, zwłaszcza, że jest to styczeń czyli ostatni miesiąc przed sesją. W momencie, gdy piszę tę notkę, upływa właśnie pierwszy tydzień tej najbardziej nielubianej przez studentów pory roku :). Wracając jednak do tematyki tego devBloga należałoby powiedzieć czym ciekawym zajmowałem się przez ten czas, przecież nie samą nauką człowiek żyje.

Postanowiłem dodać do mojego silnika możliwość wczytywania różnego rodzaju plików z meshami. Na początek napisałem prostą klasę opakowującą interfejs ID3DXMesh, w której umieściłem strukturę przechowującą materiały danego mesha. Następnie napisałem funkcje automatyzujące odczyt i zapis plików w formacie .x. Schody zaczęły się, gdy chciałem dodać obsługę dodatkowego formatu – zdecydowałem, że będą to pliki .obj (Object files). OBJ są to pliki tekstowe, przechowujące informacje o całym meshu (fajna specyfikacja tego formatu znajduje się tutaj), z którymi w parze są jeszcze pliki .mtl, gdzie trzymane są informacje o materiałach dla danego subsetu (więcej info tutaj). Najważniejszą sprawą w całym tym przedsięwzięciu było napisanie parsera dla tego formatu. Na szczęście z pomocą przychodzi Microsoft DirectX SDK, w którym znajduje się przykładowy program („MeshFromOBJ”) wczytujący pliki OBJ.

Pisząc klasy obsługujące różne modele chciałem, aby istniała możliwość konwersji między tymi formatami. Powstała więc koncepcja dwóch rodzajów klas – klasa Mesh oraz klasy obsługujące formaty. Klasy te mają być od siebie w jak największym stopniu niezależne, czyli ma istnieć możliwość dodawania funkcjonalności do klasy Mesh bez konfliktu z klasami wczytującym oraz musi istnieć możliwość pisania dodatkowych klas obsługujących różne formaty. Szczerze mówiąc, pierwszy raz zetknąłem się z takim problemem, ale na szczęście udało mi się znaleźć proste i eleganckie rozwiązanie. Mianowicie zastosowałem prosty interfejs, po którym dziedziczy klasa Mesh, z tym wyjątkiem, że klasa interfejsu posiada funkcje abstrakcyjne public, a klasa Mesh definiuje te funkcje jako protected, bądź private. Dzięki takiemu podejściu klasy korzystające z interfejsu, mogą operować na tych funkcjach, a klasa Mesh ma te funkcje ukryte :).