Przełom w świecie DirectX - DirectX.NET

Czym jest DirectX.NET

Pojawienie się w 1995 pierwszej wersji DirectX, przeznaczonej dla nowo zaprojektowanego systemu operacyjnego Windows 95, oznaczało ostateczne otwarcie się świata Windows na zaawansowane aplikacje multimedialne. Celem projektantów DirectX było stworzenie jednolitego interfejsu programowania, który pozwoliłby programistom tworzyć kod dający się uruchomić na dowolnie skonfigurowanym PC-cie.

Niemal od samego początku DirectX był przez programistów mocno krytykowany. Krytyka była szczególnie zjadliwa, gdy porównywano DirectX do OpenGL - mimo dość dużych różnic technologicznych, programistom trudno było wytłumaczyć dlaczego kod w DirectX musi być tak brzydki i zagmatwany w porównaniu z analogicznym kodem w OpenGL.

Pojawienie się platformy .NET i języka C# dawało nadzieję na zmianę tej sytuacji. Jednak okazało się, że programiści owszem, mogą korzystać z DirectX, ale wymaga to warstwy pośredniej między DirectX 8 zbudowanym w modelu COM, a kodem zarządzanym. Dopiero pojawienie się DirectX 9 oznacza prawdziwy przełom. Zgodnie z obietnicami, Microsoft dołączył do najnowszej wersji DirectX 9.0 zestaw bibliotek, umożliwiających korzystanie z DirectX bezpośrednio z poziomu kodu zarządzanego. Biblioteki te nazwano DirectX.NET.

Możliwość bezpośredniego operowania obiektami DirectX z poziomu kodu zarządzanego ma mnóstwo zalet, m.in.:

Osobiście w DirectX.NET fascynują mnie dwie rzeczy.

Po pierwsze - nareszcie mogę pisać naprawdę elegancki kod dla DirectX w C#. Z wielu powodów C# jest moim ulubionym językiem a teraz sięgam do DirectX bezpośrednio z poziomu C#. Prostota C# pozwala skupić się na problemie, który rozwiązuję, a nie walczyć z często niepotrzebnie zagmatwaną składnią C++. Jak wnikać w skomplikowane algorytmy graficzne, czy korzystać z zaawansowanych możliwości akceleratorów, kiedy przez 75 procent czasu walczy się ze wskaźnikiem na wskaźniki do tablic w pamięci, których elementy są wskaźnikami do obiektów, które są referencjami... zaraz... a może tablicami referencji... Koszmar. Tam gdzie kod C++ straszy swoją ciężkością, tam kod C# pozostaje lekki, jednolity i elegancki.

Po drugie, DirectX.NET jest nareszcie porządnym interfejsem zorientowanym obiektowo. Do tej pory interfejs DirectX był przedziwną mieszaniną funkcji globalnych, makr i klas, a wszystko to podlane było ciężkostrawnym sosem modelu COM.

Dość już magicznych funkcji do operacji na obiektach. Teraz zamiast:

D3DXMATRIX turnLeft;
D3DXMatrixRotationY( &turnLeft, -10.0 );
napiszemy:
Matrix turnLeft = Matrix.RotationY( -10.0f );
Dość już magicznych stałych, teraz zamiast:
_device->SetRenderState(D3DRS_LIGHTING, true);
napiszemy:
_device.RenderState.Lighting = true;
Dość ciągłych HRESULTów i makr SUCCEEDED/FAILED. Teraz błędy zgłaszane są za pomocą wyjątków. Dość tysięcy typów danych, jak choćby D3DCOLOR - biblioteki DirectX.NET są zintegrowane z biblioteką standardową .NET, a to oznacza że teraz użyjmy po prostu System.Drawing.Color.

A jak jest z wydajnością? Zaskakująco dobrze - zarządzany DirectX jest niewiele lub prawie wcale wolniejszy od niezarządzanego. Decydujące znaczenie dla prędkości działania kodu ma najczęściej i tak wydajność akceleratora, zaś prędkość wykonywania się samego kodu jest porównywalna.

Struktura DirectX.NET

Zarządzane biblioteki DirectX są wspólne dla wszystkich języków platformy .NET. W kolejnych przykładach będę używał C#, nic jednak nie stoi na przeszkodzie by programować na przykład w C++ czy VB.NET. Należy pamiętać o tym, że tylko w C++ można tworzyć kod DirectX "po staremu", czyli nie korzystając z obiektowych bibliotek zarządzanych.

DirectX.NET składa się z następujących komponentów:

Instalacja DirectX.NET

Biblioteki DirectX.NET instalowane są automatycznie podczas instalacji DirectX 9. Ich obecność można zbadać zaglądając do katalogu Microsoft.NET w katalogu systemowym Windows. Oprócz katalogu Framework, gdzie domyślnie instaluje się .NET Framework, powinien być tam również katalog Managed DirectX. Programiści powinni pamiętać o wybraniu odpowiedniej wersji DirectX 9: oprócz wersji standardowej, w DirectX 9 SDK znajduje się specjalna wersja umożliwiająca również śledzenie kodu DirectX z poziomu środowiska (po zainstalowaniu SDK obie wersje znajdują się odpowiednio w ..\DX9SDK\SDKDev\Retail lub ..\DX9SDK\SDKDev\Debug).

Natychmiast po zainstalowaniu DirectX9 SDK można zajrzeć do katalogu ..\Samples, gdzie znajdują się przykładowe programy w C++, C# i VB.NET. Spora część programów pojawia się we wszystkich tych językach, można więc porównać nie tylko przejrzystość kodu, ale i prędkość działania. Przykładów jest dużo i są naprawdę interesujące.

Programy DirectX.NET mogą być kompilowane zarówno z poziomu środowiska Visual Studio .NET, bezpośrednio z linii poleceń ale także z poziomu na przykład Sharp Developa, darmowego środowiska programistycznego dla platformy .NET. Dla celów kompilacji z linii poleceń przygotujmy prosty skrypt (nazwijmy go compile.bat):

csc.exe "/lib:C:\WINNT\Microsoft.NET\Managed DirectX\v4.09.00.0900" /r:Microsoft.DirectX.dll %1
Skrypt ten będziemy wołać z parametrem zawierającym nazwę kompilowanego programu. Jeśli kompilowany program będzie wymagał referencji do większej liczby bibliotek, wystarczy dodać je jako kolejne parametry (/lib:nazwabiblioteki).

Pierwszy program w DirectX.NET

Przegląd bibliotek DirectX.NET rozpoczniemy od DirectDraw, potem zobaczymy prosty przykład Direct3D a na końcu prosty przykład biblioteki AudioVideoPlayback. Przykłady użycia pozostałych bibliotek oczywiście znajdują się w SDK.

Pierwszy i najprostszy programem jaki napiszemy będzie tworzył powierzchnię DirectDraw i kopiował jej zawartość do okna. Tak naprawdę będzie nam potrzebna jedynie instancja obiektu urządzenia DirectDraw oraz obiektu opisującego powierzchnię DirectDraw.

private Device  draw      = null; 
private Surface primary   = null;
Oba obiekty są tworzone i kojarzone - urządzenie z oknem, a powierzchnia z urządzeniem:
draw = new Device(); 
draw.SetCooperativeLevel(this, CooperativeLevelFlags.Normal);
	. . .
SurfaceDescription description = new SurfaceDescription(); 
description.SurfaceCaps.PrimarySurface = true; 
primary = new Surface(description, draw);
Ponieważ powierzchnia DirectDraw jest obiektem, wszelkie operacje takie jak rysowanie, blokowanie czy zamiana stron są po prostu metodami odpowiedniego obiektu. Prosty kształt narysujemy więc za pomocą metody:
primary.DrawCircle( .... );
a tekst za pomocą metody:
primary.DrawText( ... );	
Interfejs obiektowy sprawdza się zwłaszcza w przypadku środowisk z autouzupełnianiem kodu - tam programista nie musi nawet zaglądać do dokumentacji biblioteki, ponieważ wszystkie metody obiektu pojawią się natychmiast po wpisaniu kropki po nazwie obiektu.

Powyższy przykład można bez trudu rozbudować o prostą animację, dodać podwójne buforowanie oraz wyświetlanie obrazu na pełnym ekranie. Proponuję potraktować to jako ćwiczenie, zerkając w razie potrzeby do przykładów z SDK.

Direct3D


Prosty przykład Direct3D. Obiekt wczytany z pliku .X

Direct3D jest najciekawszą częścią DirectX.NET. W każdej kolejnej wersji DirectX programiści dostają do rąk coraz potężniejsze narzędzia do tworzenia grafiki 3D. W wersji 9 możliwości są przeogromne: od tworzenia prostych obiektów, modelowania światła, tekstur, przez manipulację siatkami obiektów (vertex shading) aż do zaawansowanego nakładania tekstur (pixel shading).

Aby przekonać się jak sprawuje się obiektowy interfejs Direct3D, napiszemy prosty przykład. Z pliku załadujemy opis siatki obiektu 3d (mesh), dodamy 2 światła, kamerę i na koniec ożywimy całość dodając jakiś ruch.

Przyjrzyjmy się przykładowemu fragmentowi, który tworzy macierze widoku i perspektywy i po raz kolejny zwróćmy uwagę jak elegancko spisuje się tutaj model obiektowy DirectX.NET:

Vector3 eyePosition = new Vector3(0, 0, -20);
Vector3 direction   = new Vector3(0, 0, 0);
Vector3 upDirection = new Vector3(0, 1, 0);
Matrix view = Matrix.LookAtLH(eyePosition, direction, upDirection );
device.SetTransform(TransformType.View, view);
float fieldOfView = (float)Math.PI/4;
float aspectRatio = 1.0f;
float nearPlane   = 1.0f;
float farPlane    = 500.0f;
Matrix projection = Matrix.PerspectiveFovLH(fieldOfView, aspectRatio, nearPlane, farPlane);
device.SetTransform(TransformType.Projection, projection);
Pięknie!

Na uwagę zasługuje także nieco inna niż w typowej aplikacji okienkowej konstrukcja pętli głównej programu, dzięki której uzyskuje się maksymalną możliwą wydajność animacji.

Otóż w zwykłej aplikacji okienkowej w C# w funkcji Main pisze się najczęściej po prostu:

Application.Run( new fMain() );
Jednak przy takiej konstrukcji aby uzyskać jakikolwiek ruch musielibyśmy utworzyć zegar, ustawić go na jakiś kwant czasu i podczas obsługi zdarzenia zegara tworzyć kolejną ramkę animacji. Takie rozwiązanie ma dużą wadę: zakładamy bowiem że kwant czasu zaprogramowanego zegara odpowiada mniej więcej możliwości tworzenia płynnego obrazu przez maszynę. O wiele lepiej byłoby tworzyć obraz natychmiast po tym, kiedy skończy się tworzenie poprzedniej ramki.

W pierwszej chwili wydaje się, że wymagałoby to zejścia aż na poziom pętli obsługi komunikatów, jednak nieoczekiwane, jest to możliwe w C# na poziomie kodu obiektowego:

using ( DirectXForm dxForm = new DirectXForm() )
{
	. . .			
	dxForm.Show();
	. . .
	while ( dxForm.Created )
	{
		dxForm.AdvanceFrame();
		dxForm.Render();

		Application.DoEvents();
	}
}
Podobnie jak w przypadku DirectDraw, proponuję ten prosty przykład potraktować jako szablon do dalszych eksperymentów.

Audio Video Playback

Własny program w DirectX 9 można wyjątkowo łatwo wzbogacić o obsługę multimediów, czyli filmów i nagrań dźwiękowych. Rozpoczęcie odtwarzania filmu w oknie programu możliwe jest na przykład za pomocą jedynie 3 linijek kodu:
video       = Video.FromFile( nazwaPliku );
video.Owner = this;
video.Play();		
Gdyby przy odrobinie wysiłku dodać jeszcze wyświetlanie napisów, mielibyśmy całkiem całkiem przyzwoity program do kina domowego.

Przyszłość DirectX.NET

Czy DirectX.NET przyjmie się wśród programistów? Czy zaczną pojawiać się duże DirectXowe gry napisane w C#? Czy projektanci OpenGL podejmą wyzwanie i również przygotują biblioteki do bezpośredniej komunikacji kodu zarządzanego z OpenGL?

Bardzo chciałbym znać odpowiedzi na te pytania. Z jednej strony wielu programistów podchodzi sceptycznie nawet nie do DirectX.NET ale w ogóle do samej idei .NET. Z drugiej strony zarządzany C# jest prostszy niż C++, co w rękach doświadczonego programisty może oznaczać bardzo duży wzrost wydajności tworzenia kodu. Pamiętajmy o tym, że DirectX.NET ma raptem kilka miesięcy. Czy to oznacza, że w zaciszu swoich laboratoriów wielkie korporacje już pracują nad dużymi projektami w C# + DirectX.NET?

Zanim za parę miesięcy sami się o tym przekonamy, proponuję już wziąć sprawy w swoje ręce: w rękach nowicjusza DirectX.NET oznacza znacznie łatwiejszy dostęp do zaawansowanych możliwości programistycznych niż w chwalonym za prostotę OpenGLu; w rękach profesjonalisty DirectX.NET oznacza przejrzystszy i bardziej intuicyjny kod, do którego powrót w ramach konserwacji nie będzie żadnym problemem.

Podczas pisania tego tekstu korzystałem z różnych materiałów, m.in.:

Linki

  1. Kody źródłowe przykładowych programów