AnMaBaGiMa's Home

DirectX-Anwendungen mit C++ erstellen - Vertiefung der Möglichkeiten

Im Einführungs-Level zu DirectDraw haben wir Ihnen gezeigt wie Sie DirectDraw initialisieren, Bilder laden, Bilder anzeigen und diese Bilder auch mehr oder weniger bewegt bzw. animiert darstellen. In diesem Level werden wir nun tiefer in die Details steigen.
Unser erstes Ziel wird die Darstellung eines lodernden Feuers in einem Fenster sein. Um Ihnen das Weiterlesen noch etwas schmackhafter zu machen, hier ein kleiner Vorblick auf das, was wir anschliessend mit dem Feuer vorhaben:

Feuer in Hintergrundbild geblendet

Zunächst werden wir uns erst einmal mit dem Algorythmus der Feuerberechnung auseinandersetzen. Er ist recht einfach und stammt noch aus den Zeiten der Grafikprogrammierung mit DOS.
Das Feuer wird in einem Raster dargestellt. Dabei erhält jeder Rasterpunkt einen Wert zwischen 0 und 255. Dieser Wert repräsentiert die Temperatur des Punktes. Es ist bekanntlich unten an der Branntstelle am heissesten und kühlt nach oben hin ab. Die Temperatur eines Punktes im Raster berechnet man nun wie folgt:
Die Temperaturen dieses Punktes, des Punktes direkt unter ihm, und die davon links und rechts liegenden werden gemittelt und ergeben so die neue Temperatur des Punktes:


Die Formel auf diese Skizze bezogen würde dann lauten:
z = (z + a + b + c):4
Zusätzlich wird das Ergebnis z um 1 "abgekühlt", wenn es noch nicht die Temperatur von 0 erreicht hat.
Diese muss nun für jeden Punkt des Feuerrasters ausgeführt werden. Die Berechnung für die Punkte wird von oben nach unten erfolgen

Für ein Programm, welches ein solches Feuer berechnen soll, muss als erstes ein solches Rastere angelegt werden. Dieses Raster soll 200 Punkte breit und 100 Punkte hoch sein.
Wir erstellen zunächst ein neues Projekt mit dem Namen "TestDFire". In diesem Projekt erzeugen wir die Datei TestDFire.c

/* TestDFire.c */ #include <windows.h> #include "ddraw.h" #define FHOEHE 100 #define FBREITE 200 typedef byte FEUERBUFFER[FHOEHE][FBREITE]; FEUERBUFFER mFire;//mFire ist das Feuerraster in diesem Programm
Die am Anfang gemachten Überlegungen zur Berechnung des Feuers kommen in der Funktion CalcFire zur Anwendung.

void CalcFire(void) { int x, y; int heat; for (y=0;y<FHOEHE;y++) { for (x=0;x<FBREITE;x++) { heat = (mFire[y][x]+mFire[y+1][x]+mFire[y+1][x-1]+mFire[y+1][x+1]); heat = heat>>2; //2 Bit nach rechts schieben entspricht /4 if (heat > 0) heat--; mFire[y][x] = heat; } } }
Diese Funktion berechnet nun für jeden Punkt des Feuerrasters die neue Temperatur - aber, wenn wir diese das Erste mal aufrufen, haben alle Punkte im Raster die Temperatur 0! Das hat zur Folge das es kein Feuer gibt. Denn jede erneute Berechnung würde wieder für alle Punkte die Temperatur von 0 ergeben.
Um nun ein Feuer entstehen zu lassen , müssen wir es erst einmal entzünden. Das heisst, dass wir in der letzten Zeile des Rasters zufällige Punkte aufheizen. Die Berechnung der Temperaturen der anderen Punkte wird demnach vor dieser Zeile halt machen !

void CalcFire(void) { int x, y, i; int heat; for (i=0;i<50);i++) //50 zufällige Punkte erhitzen { //eine zufällige Position im Ratser bestimmen x = FBREITE*rand()/RAND_MAX; //Temperatur dieses Punktes in der letzen Zeile hohlen heat = mFire[FHOEHE-1][x]; //Temperatur um einen zufälligen Wert //zwischen 0 und 16 erhöhen heat+= 16*rand()/RAND_MAX; //ist die Temperatur nun grösser als 255 ? //wenn ja - komplett abkühlen auf 0 if (heat>255) heat=0; //die neue Temperatur setzen mFire[FHOEHE-1][x] = heat; } for (y=0;y<FHOEHE-1;y++) //diese Schleife nur noch bis zur vorletzten Zeile { for (x=0;x<FBREITE;x++) { heat = (mFire[y][x]+mFire[y+1][x]+mFire[y+1][x-1]+mFire[y+1][x+1]); heat = heat>>2; //2 Bit nach rechts schieben entspricht /4 if (heat > 0) heat--; mFire[y][x] = heat; } } }
Das war auch schon der Teil, der sich mit der Berechnung des Feuers beschäftigt. Viel interessanter ist natürlich die Darstellung des Feuers. Dazu wollen wir Methoden von DirectDraw verwenden.
Wie aus dem letzten Level bekannt benötigen wir als Minimum 2 Zeichenflächen und einen Clipper.

LPDIRECTDRAW7 lpDDraw7; LPDIRECTDRAWSURFACE7 lpDSurf7; //sichtbare Zeichenfläche DDSURFACEDESC2 DDSurfDesc2; LPDIRECTDRAWSURFACE7 lpDPaint7; //Arbeitszeichenfläche DDSURFACEDESC2 DDPaintDesc2; LPDIRECTDRAWCLIPPER lpDClipper;
Die Funktion InitDirectDraw kann zum grössten Teil aus dem ersten Testprogramm "TestDDraw" übernommen werden. Die Arbeitszeichenfläche lpDPaint7 soll die Grösse des Feuers haben - also 200x100.
Der Vollständigkeit halber ist hier sowohl InitDirectDraw als auch ExitDirectDraw abgebildet:

BOOL InitDirectDraw(HWND hwnd) { LPDIRECTDRAW lpDDraw; // Erzeugen der Schnittstelle zu IDirectDraw if (DirectDrawCreate(NULL, &lpDDraw, NULL)!= DD_OK) { MessageBox(hwnd, "Konnte DirectDraw-Interface nicht erzeugen.", NULL, MB_OK); return FALSE; } //IDirectDraw nach IDirectDraw7 fragen if (lpDDraw->lpVtbl->QueryInterface(lpDDraw, &IID_IDirectDraw7, (void **)&lpDDraw7) != DD_OK) { MessageBox(hwnd, "Sie haben kein DirectX - 2 installiert", NULL, MB_OK); return FALSE; } lpDDraw->lpVtbl->Release(lpDDraw); if (lpDDraw7->lpVtbl->SetCooperativeLevel(lpDDraw7,hwnd,DDSCL_NORMAL)!=DD_OK) { MessageBox(hwnd, "Konnte Kooperationslevel nicht setzen", NULL, MB_OK); return FALSE; } memset(&DDSurfDesc2, 0, sizeof(DDSURFACEDESC2)); DDSurfDesc2.dwSize = sizeof(DDSURFACEDESC2); DDSurfDesc2.dwFlags = DDSD_CAPS; DDSurfDesc2.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE |//primäre Zeichenfläche DDSCAPS_VIDEOMEMORY; DDSurfDesc2.ddsCaps.dwCaps2 = 0; if (lpDDraw7->lpVtbl->CreateSurface(lpDDraw7, &DDSurfDesc2, &lpDSurf7, NULL) != DD_OK) { MessageBox(hwnd, "Konnte primäre Zeichenfläche nicht anlegen", NULL, MB_OK); return FALSE; } if (lpDSurf7->lpVtbl->GetSurfaceDesc(lpDSurf7, &DDSurfDesc2) != DD_OK) { MessageBox(hwnd, "Surfacebeschreibung konnte nicht gelesen werden", NULL, MB_OK); return FALSE; } //Arbeitszeichenfläche erzeugen memset(&DDPaintDesc2, 0, sizeof(DDSURFACEDESC2)); DDPaintDesc2.dwSize = sizeof(DDSURFACEDESC2); DDPaintDesc2.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT; DDPaintDesc2.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN |//normale Zeichenfläche DDSCAPS_SYSTEMMEMORY; DDPaintDesc2.ddsCaps.dwCaps2 = 0; DDPaintDesc2.dwHeight = FHOEHE; DDPaintDesc2.dwWidth = FBREITE; if (lpDDraw7->lpVtbl->CreateSurface(lpDDraw7, &DDPaintDesc2, &lpDPaint7, NULL) != DD_OK) { MessageBox(hwnd, "Konnte PaintSurface nicht anlegen", NULL, MB_OK); return FALSE; } if (lpDPaint7->lpVtbl->GetSurfaceDesc(lpDPaint7, &DDPaintDesc2) != DD_OK) { MessageBox(hwnd, "Surfacebeschreibung konnte nicht gelesen werden", NULL, MB_OK); return FALSE; } if (lpDDraw7->lpVtbl->CreateClipper(lpDDraw7, 0,&lpDClipper, NULL)!=DD_OK) { MessageBox(hwnd, "Konnte Clipper nicht anlegen", NULL, MB_OK); return FALSE; } if (lpDClipper->lpVtbl->SetHWnd(lpDClipper, 0, hwnd) != DD_OK) { MessageBox(hwnd, "Konnte Clipperfenster nicht setzen", NULL, MB_OK); return FALSE; } if (lpDSurf7->lpVtbl->SetClipper(lpDSurf7, lpDClipper) != DD_OK) { MessageBox(hwnd, "Konnte Clipper nicht dem Surface zuweisen", NULL, MB_OK); return FALSE; } return TRUE; } void ExitDirectDraw(void) { if (lpDClipper!= NULL) lpDClipper->lpVtbl->Release(lpDClipper); if (lpDPaint7 != NULL) lpDPaint7->lpVtbl->Release(lpDPaint7); if (lpDSurf7 != NULL) lpDSurf7->lpVtbl->Release(lpDSurf7); if (lpDDraw7 != NULL) lpDDraw7->lpVtbl->Release(lpDDraw7); }
Die Funktion WinMain wird sich gegenüber den ersten Beispielen dieses Tutorials auch nur wenig verändern. Die Fenstergrösse wird fest und unveränderlich gewählt und die Hauptnachrichtenschleife wird ihr Aussehen etwas ändern. Diese Änderung ist notwendig, da immer dann, wenn gerade keine Nachricht für die Anwendung ansteht, das Feuer neu berechnet werden soll um Bewegung in dieses zu bekommen. Dennoch soll die Anwendung auf Benutzereingaben - wie Anwendung schliessen - reagieren können.

BOOL APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { WNDCLASS wc; HWND hwnd; MSG msg; wc.lpszClassName = "TestDFire"; wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WindowProc; // siehe unten wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH) (COLOR_BACKGROUND + 1); wc.lpszMenuName = NULL; RegisterClass(&wc); hwnd = CreateWindow ("TestDFire", "Feuer-Simulation", WS_OVERLAPPED, 0, 0, FBREITE + 6, FHOEHE + 25,//für Titelzeile und Ränder NULL, NULL, hInstance, NULL); ShowWindow (hwnd, SW_SHOW); //nur wenn DDraw erfolgreich initialisiert wurde weiter machen ! if (InitDirectDraw(hwnd) == TRUE) { UpdateWindow (hwnd); while (TRUE) { if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) { if (!GetMessage(&msg, NULL, 0, 0)) break; TranslateMessage(&msg); DispatchMessage(&msg); } else { CalcFire(); //Feuer berechnen } } } ExitDirectDraw(); return msg.wParam; }
Der nächste Schritt wird es nun sein, dass wir das Feuer, welches in seinem Puffer "lodert" in die Arbeitszeichenfläche übertragen. Die Technik die wir dafür verwenden wollen ähnelt der, die wir im DDraw-Einführungs-Level benutzt haben, um eine waagerechte Linie zu zeichnen.
Wir werden uns also mit der Methode Lock der Zeichenfläche einen Zeiger auf ihre Bilddaten besorgen und Byte für Byte aus dem Feuerpuffer in die Zeichenfläche übertragen.

void DrawFire(void) { int x,y; BYTE *lpByte; if (lpDPaint7->lpVtbl->Lock(lpDPaint7, NULL, &DDPaintDesc2, DDLOCK_WAIT, NULL) == DD_OK) { lpByte = DDPaintDesc2.lpSurface; for (y=0;y<FHOEHE;y++) { for (x=0;x<FBREITE;x++) { *lpByte = mFire[y][x];//Byte aus Feuerpuffer übertragen lpByte++; //nächstes Byte } //Zeiger auf nächsten Zeilenanfang in der Zeichenfläche setzen lpByte+=(DDPaintDesc2.lPitch - FBreite); } lpDPaint7->lpVtbl->Unlock(lpDPaint7, NULL); } }
Der Wert in lPitch der Struktur DDPaintDesc2 gibt den Abstand der Zeilenanfänge in Byte an. Um also zur nächsten Zeile zu gelangen müssen wir den Zeiger um diesen Wert erhöhen und die bereits dargestellten Bytes in ihrer ANzahl abziehen.
Die Funktion DrawFire wird nun innerhalb der Hauptnachrichtenschleife direct nach CalcFire aufgerufen. Anschliessend bringen wir das Fenster noch dazu seinen Inhalt neu zu zeichnen in dem wir eine WM_PAINT-Message senden.

while (TRUE) { if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) { if (!GetMessage(&msg, NULL, 0, 0)) break; TranslateMessage(&msg); DispatchMessage(&msg); } else { CalcFire(); DrawFire(); SendMessage(hwnd, WM_PAINT, 0, 0); } }
Die Behandlung dieser Message innerhalb der Nachrichtenbehandlungsroutine WindowProc scheint trivial. Es wird die Arbeitszeichenfläche in die sichtbare Zeichenfläche geblittet.

LRESULT WINAPI WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { PAINTSTRUCT ps; RECT SRect, DRect; switch (msg) { case WM_PAINT: BeginPaint(hwnd, &ps); SRect.top = 0; SRect.left = 0; SRect.right = FBREITE; SRect.bottom = FHOEHE; GetClientRect(hwnd, &DRect); MapWindowPoints(hwnd, NULL, (LPPOINT)&DRect, 2); lpDSurf7->lpVtbl->Blt(lpDSurf7, &DRect,lpDPaint7, &SRect, DDBLT_WAIT, NULL); EndPaint(hwnd, &ps); break; case WM_DESTROY: PostQuitMessage(0); return TRUE; default: return DefWindowProc(hwnd, msg, wParam, lParam); } return FALSE; }
Nun können wir zum ersten Test schreiten und mal schauen wie das Feuer so aussieht. Jenachdem welche Farbauflösung Sie gerade eingestellt haben, wird das Ergebnis entweder so:

bei 16 Bit Farbtiefe
oder eher so aussehen:

bei 24 Bit Farbtiefe

Das hat keineswegs irgend etwas von einem Feuer.
Das Problem sind eindeutig die Farben. Aber wie genau wir dem Feuer den richtigen "Anstrich" verpassen, klären wir im nächsten Teil.

Hier gibts ersteinmal wieder den Quelltext dieser Fassung zum download (zip-Datei).