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:
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
#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>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>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;ylpVtbl->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).