Tabla de Contenidos

DirectDraw

DirectDraw es un manejador de memoria de vídeo con el cual puedes manipular directamente, desde esta, imágenes y mostrarlas de forma eficiente por pantalla.

Inicialización y configuración

Comenzando

Inicialización

Para configurar e iniciar la programación con DirectDraw tendremos que seguir los siguientes pasos, cada uno de ellos se basan en llamadas a funciones que devuelven una flag que si es distinta de DD_OK significa que ese paso ha fallado. DD_OK es del tipo HRESULT.

  1. Crear un objeto DirectDraw. Utilizaremos la función DirectDrawCreateEx pasándole 4 parámetros:
    • El GUID de la gráfica, esto es su identificador (si lo pasamos como NULL será el por defecto, aunque también podemos pasar las siguientes flags: DDCREATE_EMULATIONONLY, para modo emulado, y DDCREATE_HARDWAREONLY, sin emulación (DirectDraw no emula lo que no existe en hardware)).
    • Referencia al LPDIRECTDRAW7 (objeto DirectDraw) que crearemos, debe ser casteado como (void**).
    • IID_IDirectDraw7, que es un identificador de clase. :?:
    • Un valor NULL (realmente es para compativilidades futuras con objetos COM, pero no se utiliza).
  2. Configurar el modo coperativo. Esto determina cómo interactuaremos con el hardware. Para realizar esta configuración, el objeto DDraw contiene un método llamado SetCooperativeLevel al cual le pasas el HWND de la ventana y los flags de configuración, estos son:
    • DDSCL_EXCLUSIVE, que hace que únicamente DDraw se encargue de la gestión de gráficos, excluyendo GDI. Se ha de utilizar con DDSCL_FULLSCREEN.
    • DDSCL_FULLSCREEN, hace que la ventana se muestre a pantalla completa. Se ha de utilizar con DDSCL_EXCLUSIVE.
    • DDSCL_NORMAL, hace que DDraw se gestione en modo ventana.
    • DDSCL_ALLOWREBOOT, permite al usuario realizar un ctrl+alt+supr cuando ejecute tu aplicación.
  3. Configurar el modo de pantalla. Esto es indicar resolución y profundidad de color, si retorna DDERR_INVALIDMODE significa que la máquina que corre el programa no soporta este modo. Puedes utilizar el método del objeto DDraw EnumDisplayModes para ver los soportados.
#include <ddraw.h>
...
#define MSGBOX(body) MessageBox(NULL,body,"",MB_OK);
...
LPDIRECTDRAW7 lpdd;
...
if (DirectDrawCreateEx(NULL, (void**)&lpdd, IID_IDirectDraw7, NULL) != DD_OK) 
	MSGBOX("Falla la creación de DDraw!");
if (lpdd->SetCooperativeLevel (hwnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN) != DD_OK) 
	MSGBOX("Falla el cambio de modo a cooperativo!");
if (lpdd->SetDisplayMode (640, 480, 32, 0, 0) != DD_OK) 
	MSGBOX("Falla la asignación del modo de pantalla!");

Creando superficies

Existen dos tipos de superficies que una aplicación que implemente doble buffer con DDraw debe tener:

Recuerda, una superficie es del tipo LPDIRECTDRAWSURFACE7.

LPDIRECTDRAWSURFACE7 primarySurface;
LPDIRECTDRAWSURFACE7 backSurface;

Una vez tengamos definidas los objetos correspondientes a las superficies de DDraw necesitaremos configurarlos:

  1. Definiremos un DDSURFACEDESC2 y lo configuraremos indicando su tamaño (dwSize), las flags de configuración en la propiedad dwFlags (DDSD_CAPS y DDSD_BACKBUFFERCOUNT indican que se usará backbuffer y para indicar las capacidades de la superficie una estructura DDSCAPS), si indicamos que utilizaremos un DDSCAPS también tendremos que indicar las flags para esta que es más de lo mismo para definir la superficie (recomendadas: DDSCAPS_PRIMARYSURFACE (indica que es la superfice primaria), DDSCAPS_FLIP (le dá la capacidad de flipping (intercambio de buffers)) y DDSCAPS_COMPLEX (superficie raíz)) y mediante la propiedad dwBackBufferCount indicamos que sólo tendrá un backbuffer, pero podemos poner más.
  2. Añadiremos el DDSURFACEDESC2 a la superficie principal mediante el método de esta CreateSurface.
  3. Definiremos un DDSCAPS2 para asignarselo a la superficie del BackBuffer. Lo configuraremos indicando en su propiedad dwCaps la flag DDSCAPS_BACKBUFFER.
  4. Asignaremos el anterior DDSCAPS2 a la superficie del BackBuffer y esta la asignaremos a la principal como tal mediante la función de la superficie primaria GetAttachedSurface.
DDSURFACEDESC2 ddsd;
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX;
ddsd.dwBackBufferCount = 1;
 
if (lpdd->CreateSurface (&ddsd, &primarySurface, NULL) != DD_OK) 
	MSGBOX ("Falla la creación de la superficie principal");
 
DDSCAPS2 ddscaps;
ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
if (primarySurface->GetAttachedSurface(&ddscaps, &backSurface) != DD_OK) 
   MSGBOX ("Falla el enlace con el backbuffer");

* ZeroMemory limpia la memoria para DDSURFACEDESC2, de esa forma no tendremos problemas más tarde.

Cerrando DirectDraw

Una vez acabemos una aplicación de DDraw debemos dejarlo todo tal cual estaba, para ello seguiremos los siguientes pasos:

  1. Dejar el modo de pantalla como estaba antes llamando al método RestoreDisplayMode del objeto DirectDraw.
  2. Dejar el nivel cooperativo en normal volviendo a utilizar el método SetCooperativeLevel pero esta vez pasándole la flag DDSCL_NORMAL.
  3. Liberar la superficie primaria utilizando su método Release.
  4. Liberar las demás superficies utilizadas para cargar imágenes (método Release).
  5. Liberar el objeto DirectDraw también mediante su método Release.
lpdd->RestoreDisplayMode();
lpdd->SetCooperativeLevel(hwnd, DDSCL_NORMAL);
primarySurface->Release();
lpdd->Release();

Cuando se pierden las superficies...

Cuando se pierden las superficies… Hay que recuperarlas.
Esto ocurre, por ejemplo, cuando el usuario cambia de aplicación mediante un alt+tab o apretando a la tecla de windows. La aplicación pasa a segundo plano y al retornar GDI vuelve a quedarse con el dominio de la ventana, se han borrado las superficies! Cuando esto ocurre los métodos Flip o BltFast ya no retornan DD_OK sino DDERR_SURFACELOST. Aún así podemos recuperarlas fácilmente: únicamente se ha de llamar al método Restore de cada superficie (o si lo preferimos al RestoreAllSurfaces del objeto DDraw), pero al recuperarlas ya no tienen cargadas las imágenes, tendremos que volver a cargarlas de nuevo.

Haciendo cosillas

Mostrar una imágen

Para DDraw una imágen también es una superficie, por lo que los pasos para mostrar una es cargarla y luego volcarla sobre un objeto LPDIRECTDRAWSURFACE. Algo fácil si seguimos los siguientes pasos:

BITMAP bmp;
HBITMAP hBitmap = (HBITMAP)LoadImage(NULL, pathBMP, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_CREATEDIBSECTION);
GetObject(hBitmap, sizeof(bmp), &bmp);
LPDIRECTDRAWSURFACE7 bmpSurface;
DDSURFACEDESC2 ddsd;
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
ddsd.dwWidth = bmp.bmWidth;
ddsd.dwHeight = bmp.bmHeight;
lpdd->CreateSurface(&ddsd, &bmpSurface, NULL);
HDC hdcImagen = CreateCompatibleDC(NULL);
HDC hdc; 
HBITMAP hBitmapAnt = (HBITMAP)SelectObject(hdcImagen, hBitmap); 
bmpSurface->GetDC(&hdc); 
BitBlt(hdc, 0, 0, bmp.bmWidth, bmp.bmHeight, hdcImagen, 0, 0, SRCCOPY); 
bmpSurface->ReleaseDC(hdc); 
SelectObject(hdcImagen, hBitmapAnt); 
DeleteDC(hdcImagen); 
DeleteObject(hBitmap);
RECT rectOrig;
rectOrig.left = 0;
rectOrig.top = 0;
rectOrig.right = 640;
rectOrig.bottom = 480;
backSurface->BltFast(0, 0, bmpSurface, &rectOrig, DDBLTFAST_WAIT);
primarySurface->Flip(backSurface, DDFLIP_WAIT);

Utilizar funciones de GDI

Una vez tenemos las superficies bien montaditas y configuraditas podemos utilizar las funciones de GDI para escribir sobre ellas, es decir, podemos escribir textos, dibujar y todas esas cosas tan monas que hacemos sobre GDI pero esta vez sobre las superficies de DDraw.

primarySurface->Flip(backSurface, DDFLIP_WAIT);
HDC hdc;
if (Flip(backSurface->GetDC(&hdc) == DD_OK) { 
    SetBkColor(hdc, RGB(0, 0, 255)); 
    SetTextColor(hdc, RGB(0, 0, 0)); 
    TextOut(hdc, 0, 0, "hola", lstrlen("hola")); 
    Flip(backSurface->ReleaseDC(hdc); 
}

Color Keying

El color keying es el concepto de indicar a DDraw que en toda una imágen un determinado color funcionará como transparente. De esa forma, cuando nosotros mostremos dicha imágen, las partes con ese color no se veran.
Para poder realizar el color keying es necesario un objeto del tipo DDCOLORKEY y dar a este su valor bajo (dwcolorSpaceLowvalue) y valor alto (dwcolorSpaceHighValue), que serán utilizados como segun lo asignemos a la superficie. Para ello, asignarlo, llamamos al método: SetColorKey pasandole los flags deseados y un puntero al DDCOLORKEY.

Por ejemplo, para asignar al verde una transparencia:

DDCOLORKEY ddck;
ddck.dwColorSpaceHighValue = 0x00FF00;
ddck.dwColorSpaceLowValue = 0x00FF00;
 
bmpSurface2->SetColorKey(DDCKEY_SRCBLT, &ddck);

No debemos olvidar ahora que al volcar la imágen sobre la superficie se ha de indicar que se tiene que tener en cuenta el color keying:

backSurface->BltFast(340, i, bmpSurface2, &rectOrig2, DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY);

Asignar el color key en la descripción de la superficie

Hacer esto no es tarea compleja, únicamente debemos añadir dos valores más a las propiedades del DDSURFACEDESC2, la ddckCKSrcBlt.dwColorSpaceLowValue y la ddckCKSrcBlt.dwColorSpaceHighValue correspondientes a los de un DDCOLORKEY. También tendremos que indicar mediante una flag más (DDSD_CKSRCBLT) que se tenga en cuenta este color key.

DDSURFACEDESC2 ddsd;
ZeroMemory(&ddsd, sizeof(ddsd));
ddsd.dwSize = sizeof(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_CKSRCBLT;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN;
ddsd.dwWidth = bmp.bmWidth;
ddsd.dwHeight = bmp.bmHeight;
ddsd.ddckCKSrcBlt.dwColorSpaceLowValue  = 0x00ff00;
ddsd.ddckCKSrcBlt.dwColorSpaceHighValue  = 0x00ff00;

Blitting

El blitter es la parte de la gráfica que puede manipular los datos de las imágenes, para acceder a él mediante DirectDraw se utilizan los método Blt y BltFast de una superficie. Con el primero, Blt, puedes realizar más operaciones que con BltFast pero también es más lento, veamos qué parámetros necesita:

DDBLTFX l;
ZeroMemory(&l, sizeof(l));
l.dwSize = sizeof(l);
l.dwFillColor = 0x0000ff;
backSurface->Blt(NULL, NULL, NULL, DDBLT_COLORFILL, &l);
backSurface->Blt(NULL, bmpSurface, NULL, DDBLT_WAIT, NULL);
RECT rectOrig2, rectOrig3;
 
rectOrig2.left = 50;
rectOrig2.top = 50;
rectOrig2.right = 100;
rectOrig2.bottom = 100;
 
rectOrig3.left = 0;
rectOrig3.top = 0;
rectOrig3.right = 100;
rectOrig3.bottom = 100;
 
backSurface->Blt(&rectOrig2, bmpSurface, &rectOrig3, DDBLT_WAIT, NULL);
DDBLTFX l;
ZeroMemory (&l, sizeof(l));
l.dwSize = sizeof(l);
 
RECT rectOrig2;
rectOrig2.left = 0;
rectOrig2.top = 0;
rectOrig2.right = 100;
rectOrig2.bottom = 100;
 
backSurface->Blt(&rectOrig2, bmpSurface2, NULL, DDBLT_WAIT, &l);
// Basándonos en el código anterior
l.dwDDFX = DDBLTFX_MIRRORLEFTRIGHT;
backSurface->Blt(&rectOrig2, bmpSurface2, NULL, DDBLT_WAIT | DDBLT_DDFX, &l);
// Basándonos en el código anterior
DDCOLORKEY ddck;
ddck.dwColorSpaceHighValue = 0x00FF00;
ddck.dwColorSpaceLowValue = 0x00FF00;
l.ddckSrcColorkey = ddck; 
backSurface->Blt(&rectOrig2, bmpSurface2, NULL, DDBLT_WAIT | DDBLT_KEYSRC, &l);

El BltFast sirve también para copiar de una superficie a otra pero lo hace de forma más rápida que el Blt y sin aplicar efectos. Sus parámetros son los siguientes:

Crear una animación

El truco para crear una animación de sprites está en el rectángulo que indica la zona de la superficie que se dibujará. Cuando tenemos una superficie con un imágen cargada tenemos que indicar al método BltFast de la superficie donde queramos cargarla un rectángulo y la posición donde se dibujará. La gracia es que el rectángulo vaya moviendose por una gran imágen donde esten los frames de la animación del sprite pero dibujándose siempre en el mismo sitio.
Por ejemplo, imaginemos una imágen que contiene 4 dibujos de una célula, estos corresponden a una rotación donde llega hacer una vuelta completa en estos 4 dibujos\frames. La imágen es de 400×100 (donde cada célula ocupa 100×100). Modificaremos el rectángulo de la siguiente forma:

RECT rectOrig;
int y = 0;
...
rectOrig.left = y;
rectOrig.top = 0;
rectOrig.right = y + 100;
rectOrig.bottom = 100;

Y cada vez que movamos la célula haremos:

y = (y == 300) ? 0 : y + 100;

Pero siempre la dibujaremos en el mismo sitio:

backSurface->BltFast(0, 0, bmpSurface, &rectOrig, DDBLTFAST_WAIT | DDBLTFAST_SRCCOLORKEY);

Clipping

Esto es hacer que en una superficie sólo se muestre una porción, sería algo así como aplicar una máscara a esta. Para poder realizar esto necesitamos un objeto LPDIRECTDRAWCLIPPER que se crea mediante el método CreateClipper de un objeto LPDIRECTDRAW7, a este hay que pasarle 3 argumentos, pero únicamente el 2º es importante, que es un puntero a un objeto LPDIRECTDRAWCLIPPER (el primero se pasará como 0 y el tercero como NULL).

LPDIRECTDRAWCLIPPER clipper;
lpdd->CreateClipper(0, &clipper, NULL);

Una vez tengas el objeto necesitarás asignarle una lista de regiones visibles, esto se hace mediante la estructura RGNDATA, esta contiene dos elementos: Buffer (donde se guardarán los RECTs utilizados para indicar las regiones) y una estructura RGNDATAHEADER que indica cómo se debe tratar ese Buffer. Reservaremos el tamaño del buffer con malloc (y no nos olvidaremos de liberar esa porción de memoria, verdad?) ya que es un array y una vez que asignemos las regiones deberemos hacerlo de arriba abajo.
La RGNDATAHEADER contiene las siguientes propiedades útiles:

LPRGNDATA lpClipList = (LPRGNDATA)malloc(sizeof(RGNDATAHEADER) + sizeof(RECT));
RECT rcClipRect = {300,300,640,480};
memcpy(lpClipList->Buffer, &rcClipRect, sizeof(RECT));
lpClipList->rdh.dwSize = sizeof(RGNDATAHEADER);
lpClipList->rdh.iType = RDH_RECTANGLES;
lpClipList->rdh.nCount = 1;
lpClipList->rdh.nRgnSize = sizeof(RECT);
lpClipList->rdh.rcBound = rcClipRect;

Una vez tengamos la ClipList tendremos que asignarla al clipper mediante el método de este SetClipList. Y para finalizar asignar el clipper a la superficie deseada con el método de la superficie SetClipper.

clipper->SetClipList(lpClipList, 0));
backSurface->SetClipper(clipper));
free(lpClipList);

:!: No funciona del todo bien

Notas

if (FAILED(clipper->SetClipList(lpClipList, 0))) ...

Documentos