Como vimos en la anterior entrega, existen 2 posibilidades a la hora de trabajar con gráficos en PC. La primera de ellas consiste en utilizar un modo de vídeo específico bajo unas condiciones predeterminadas al desarrollar nuestra aplicación o juego. Esto es lo que ocurre cuando se decide utilizar el modo 13h, o modos SVGA de 8 bits por pixel. La segunda consiste en adaptarse a las características del sistema durante el proceso de carga de la aplicación.
Mediante este método de trabajo limitamos la posibilidad de utilizar aplicaciones windowed. Bajo Windows95, si diseñamos una aplicación GDI (que use el Interface de Dispositivo Gráfico estándar de Windows), el mismo GDI es capaz de hacer conversión automática al realizar el volcado del bitmap si la profundidad de color (bpp) del DC (contexto de dispositivo) destino es diferente de la origen (mediante BitBlt()). Por supuesto, esto resultará más lento que realizar una sola conversión durante el inicio de la aplicación, como se comentó en la anterior entrega.
En diferentes tarjetas los modos HiColor (15/16bpp), pueden definirse mediante 5 bits para la componente rojo (R), 5 para la verde (G) y 5 para la azul (B) (5:5:5 = 15 bits), aunque también es muy común encontrar tarjetas que trabajen con un sistema de 5:6:5 (6 bits para la componente verde). Además, la tarjeta puede trabajar con RGB (atendiendo a cómo están situadas las componentes dentro del word o dword) o BGR, lo cual obliga a definir diferentes modos de trabajo según las características de la misma, así como saber convertir los gráficos entre estos formatos. Para comprender los diferentes modos vamos a ver cómo trabaja la primitiva PutPixel() dentro de ellos.
// tamaño de la pantalla/buffer #define ANCHO 640 #define ALTO 480 byte buffer[ANCHO*ALTO] ; void PutPixel_8bpp( x, y, byte color ) { buffer[(y*ANCHO)+x] = color ; }La lectura es igual de sencilla, para el acceso a un elemento (x,y) basta con aplicar una conversión directa de ambas coordenadas sabiendo la anchura lógica de cada scanline del buffer/pantalla.
// Colocación de cada componente: #define MAKERGB_15bpp(r,g,b) (word) ((r<<10)|(g<<5)|(b)) word buffer[ANCHO*ALTO] ; void PutPixel_15bpp( x, y, int r, int g, int b ) { buffer[((y*ANCHO)+x)] = MAKERGB_15bpp(r, g, b) ; }Lo primero que notamos en el listado anterior es que el buffer se ha declarado usando componentes de tipo short (2 bytes), ya que en 15bpp cada pixel es representado por 2 bytes (16/8=2). Siguiendo con el ejemplo, puede observarse que se ha definido una macro MAKERGB que acepta las 3 componentes (con valores entre 0 y 31 cada una (5 bits)), y construye un word (color/pixel) mediante desplazamientos y operaciones lógicas OR (|). Vamos a examinar detenidamente dicha macro:
Supongamos que queremos trazar un pixel de valores RGB (16,9,1). Con esos datos, ejecutamos una llamada a la función PutPixel:
PutPixel_15bpp( x, y, 16, 9, 1 );Para la llamada a la macro MAKERGB se tiene que :
r = 16 = 10000b g = 9 = 01001b b = 1 = 00001bEl contenido final del byte a escribir (5:5:5) ha de quedar de la siguiente manera (debido a la organización interna del modo):
(555RGB): bit 15 -> 0rrrrrgggggbbbbb <- bit 0Aprovechando los desplazamientos (<<) de bits y la operación OR (que permite introducir valores sin modificar los bits existentes aparte de los activados ya en la variable), se deduce que:
color = (r<<10) | (g<<5) | (b) ;Los desplazamientos dejan cada componente en el lugar que le corresponde dentro del word final, y las operaciones OR realizan la composición de todas las componentes en un sólo elemento de memoria.
El resultado final es, pues, 0100000100100001b, que es el parámetro pasado a la función PutPixel. Esta función se encarga simplemente de almacenar este valor en el elemento correspondiente del array de tipo word.
Nótese que las funciones que se están analizando son todos en formato RGB. Para trazar el mismo pixel en una tarjeta BGR basta con cambiar los valores de los shifts a ((b<<16)|(g<<8)|(b)), aunque luego veremos funciones de conversión para este tipo de modos.
// Colocación de cada componente: #define MAKERGB_16bpp(r,g,b) (word) ((r<<11)|(g<<5)|(b))La rutina PutPixel() realiza el mismo trabajo que la anterior, almacenar el word resultante en el buffer de trabajo/vram.
a). Tratado individual de las componentes:
#define NUMBPP 3 char buffer[ANCHO*ALTO*NUMBPP] ; void PutPixel_24bpp_1( x, y, byte r, byte g, byte b ) { long offset = ((y*ANCHO)+x)*NUMBPP; buffer[offset] = r; buffer[offset+1] = g; buffer[offset+2] = b; }Se define un array con el tamaño necesario para todos los elementos (ancho*alto*3bytesporpixel), se calcula el offset perteneciente a dicho pixel ((y*ancho+x)*3), y se almacenan en memoria las 3 componentes del color especificado.
b). Aglutinación de las componentes:
char buffer[ANCHO*ALTO*NUMBPP] ; // Colocación de cada componente: #define MAKERGB(r,g,b) (dword) ((r<<16)|(g<<8)|(b)) void PutPixel_24bpp_2( x, y, dword color ) { long offset = ((y*ANCHO)+x)*NUMBPP; buffer[offset] = (byte) (r>>16) & 255; buffer[offset+1] = (byte) (g>>8) & 255; buffer[offset+2] = (byte) (b & 255); }En esta rutina pasamos el color ya compuesto como parámetro para descomponerlo en los bytes individuales. Para ello desplazamos la componente deseada a los 8 últimos bits del word y hacemos un AND con 255, para poner a cero todos los bits del long excepto los 8 últimos (255=11111111b), que convertimos a byte para escribir en el buffer (aunque el typecast ya estaría implícito en la escritura).
dword buffer[ANCHO*ALTO] ; // Colocación de cada componente: #define MAKERGB(r,g,b) (dword) ((r<<16)|(g<<8)|(b)) void PutPixel_32bpp( x, y, dword color ) { buffer[((y*ANCHO)+x)] = color; }Es el modo más sencillo de trabajar (aparte del de 8bpp), el más completo (16,7 millones de colores) y el que más memoria necesita (hasta 4 veces más).
También nos encontramos con el típico problema de paletización de bitmaps. Imaginemos 2 sprites cada uno de ellos con su paleta propia y se desea visualizarlos juntos en pantalla. Al no tener idéntica paleta deberemos proceder a construir una paleta óptima para ambos y mapear los colores de los sprites a esta paleta (esto es engorroso, por ejemplo, para un juego aunque no lo hagamos en tiempo real).
Precisamente lo contrario ofrecen los modos de 16bpp ó más. A cambio de requerir una mayor cantidad de videomemoria (y, por lo tanto, menor nº de fps al haber más datos, más pérdidas de caché, etc), ofrecen la individualidad de cada pixel en pantalla, lo que nos permitirá modificar la intensidad/color de un pixel sin modificar los colores similares en la imagen (es decir, ya no es posible modificar la paleta puesto que no existe). Esto da la posibilidad de realizar efectos de luces/sombras añadiendo/restando valores constantes a todos los pixels de un sprite en pantalla, como los conocidos flares que pueden verse en muchas demos/juegos.
Si sabemos que todos los bitmaps/sprites de nuestra aplicación son de una anchura múltiplo de 4, al trazarlos se leerán 4 bytes (un dword) y se escribirá éste en el buffer destino.
En el caso de 15/16bpp, tratar de escribir 2 pixels cada vez utilizando para ello un dword (2 pixels x 2 bytes = 4 bytes).
En los modos de 24bpp, escribir (si es posible) 4 pixels cada vez, utilizando para ello 3 dwords (4 pixels x 3 bytes = 12 bytes=3dw).
De esta manera además de hacer accesos a memoria más rápidos se estará desenrrollando el bucle a 1/4, 1/2 ó 1/3 de las iteraciones.
Al trazar un sprite por medio de instrucciones MOVSD (o por medio de un bucle for() con aritmética de punteros a long), si la posición del sprite es, por ej., (1,0) (desalineado), se nos penalizará con 3 ciclos de reloj por cada elemento que escribamos a memoria (por eso existe en los compiladores una opción de alineación de datos a 1/2/4 bytes).
Una forma de solucionar esto es tener 4 rutinas diferentes ((DrawSpr0 a DrawSpr3) que dependiendo del resto de la coordenada X destino del sprite (0,1,2,3)), escriban los pixels desalineados como bytes y el resto como dwords. Se debe llamar a una u otra en función de la coordenada x del sprite (esto, es, NumRutina = SpriteX & 3, o NumRutina = SpriteX % 4 ; (el resto de dividir por 4)).
La primera de ellas, DrawSpr0, debe ser llamada cuando la coord. X es múltiplo de 4 (spriteX & 3 da 0 cuando ocurre esto). Esta se encarga de volcar dwords sin preocuparse, pues el destino está alineado a 4.
A modo de ejemplo, supongamos que trazamos el sprite en (3,0). La rutina llamada será DrawSpr3, que deberá escribir 1 byte del sprite en memoria, y en ese momento, el siguiente pixel ya estará en SpriteX = 4 (alineado), por lo que podemos volcar el resto del sprite con dwords (cuidado con los últimos bytes del mismo). De similar manera habremos de crear DrawSpr1 (escribir primero 3 bytes para alinear, y luego dwords) y DrawSpr2 (escribir 2 bytes y luego dwords).
b). Uso de FPU o MMX: Puede usarse tanto la FPU (coprocesador matemático) como las instrucciones MMX para copiar 64 bytes de golpe (más rápido que el equivalente en movsd) de una posición de memoria a otra. Veamos un ejemplo de copia con FPU:
fild qword ptr [eax] fild qword ptr [eax+8] fild qword ptr [eax+16] fild qword ptr [eax+24] fild qword ptr [eax+32] fild qword ptr [eax+40] fild qword ptr [eax+48] fild qword ptr [eax+56] fxch st(1) fistp qword ptr [ebx+48] fistp qword ptr [ebx+56] fistp qword ptr [ebx+40] fistp qword ptr [ebx+32] fistp qword ptr [ebx+24] fistp qword ptr [ebx+16] fistp qword ptr [ebx+8]El anterior fragmento de código copia 64 bytes desde la dirección apuntada por EAX a EBX. Las instrucciones FILD (Fpu Integer LoaD) almacenan 8 bytes cada una en la pila del coprocesador, y las instrucciones FISTP (Fpu Integer Store and Pop) saca el último valor de la pila de la fpu y lo almacena (como un integer) en la posición indicada. La primera instrucción de almacenamiento (fistp) se ha intercambiado por la segunda (+56 por +48), para no producir 2 accesos seguidos a la misma posición de la pila, y, por tanto, no obtener ninguna penalización. Este tipo de copia es muy recomendable con buffers de volcado grandes (640x480x16bpp), tan sólo hay que utilizar un bucle en que incrementemos EBX y EAX en 64 hasta realizar el volcado.
LISTADO 1: Conversion 32bpp -> Xbpp: //----------------------------------------------------- short Convert32To15( long color ) { long result, r, g, b; r = (color>>16) & 255; g = (color>>8) & 255; b = color & 255; r = r>>3; g = g>>3; b = b>>3; result = (r<<10) | (g<<5) | b; return( (short) result ); } //------------------------------------------------------ short Convert32To16( long color ) { long result, r, g, b; r = (color>>16) & 255; g = (color>>8) & 255; b = color & 255; r = r>>3; g = g>>2; b = b>>3; result = (r<<11) | (g<<5) | b; return( (short) result ); } //----------------------------------------------------- long Convert32To24( long color ) { return( color & 0x00FFFFFF ) ; }
LISTADO 2: Conversión Xbpp -> 32bpp //----------------------------------------------------- long Convert8To32bppRGB( long colorRGB ) { long result, r, g, b; r = (colorRGB>>16) & 255; g = (colorRGB>>8) & 255; b = colorRGB & 255; result = (r<<16) | (g<<8) | b; return( result ); } //----------------------------------------------------- long Convert15To32bppRGB( short color ) { long result, r, g, b; r = (color>>10) & 31; r = (r<<3) | (r>>2); g = (color>>5) & 31; g = (g<<3) | (g>>2); b = color & 31; b = (b<<3) | (b>>2); result = (r<<16) | (g<<8) | b; return( result ); } //----------------------------------------------------- long Convert16To32bpp( short color ) { long result, r, g, b; r = (color>>11) & 31; r = (r<<3) | (r>>2); g = (color>>6) & 63; g = (g<<2) | (g>>4); b = color & 31; b = (b<<3) | (b>>2); result = (r<<16) | (g<<8) | b; return( result ); } //----------------------------------------------------- long Convert24To32bpp( long color ) { return( color & 0x00FFFFFF ) ; }Un sencillo problema surge en las conversiones de 5 ó 6 bits a 8. Como puede verse en Convert15bppTo32bpp(), no basta con desplazar la componente 3 bits (8-5=3), porque haciendo esto dejamos 3 bits a cero en la parte inferior del byte :
11111 red << 3 = 11111000Esos 3 ceros son incorrectos y los hemos de cubrir con la parte superior de la componente (antes de desplazarla), de esta manera (5-3=2):
Red = (red<<3) | (red>>2) ;Y de la misma manera con el resto de componentes y conversiones.
LISTADO 3: Conversión RGB <-> BGR //----------------------------------------------------- long Convert32bppRGBToBGR( long color ) { long result, r, g, b; r = (color>>16) & 255; g = (color>>8) & 255; b = color & 255; result = (b<<16) | (g<<8) | r; } //----------------------------------------------------- long Convert32bppRGBToBGR( long color ) { long result, r, g, b; r = color & 255; g = (color>>8) & 255; b = (color>>16) & 255; result = (r<<16) | (g<<8) | b; }
Para averiguar el nº de bits por pixel en que se está trabajando bajo Windows (bajo DOS lo seleccionamos nosotros mismos) simplemente hemos de obtener la información del contexto de dispositivo mediante:
numbpp = GetDeviceCaps( hdc, BITSPIXEL );Respecto al tipo de modo (555, 565, RGB, etc.), para Vesa/MSDOS, en el ModeInfoBlock (de cada modo de vídeo), disponemos de las siguientes variables para realizar la identificación del modo de display:
char RedMaskSize, RedFieldPosition; (e igual con Green y Blue)La primera indica el nº de bits para esa componente (por ejemplo, RedMaskSize es 5 para el modo 5:5:5), y la segunda el lugar donde está situada esa componente (para el shift). Para RedFieldPosition en 5:6:5 obtenemos 11 para RGB y 0 para BGR. De esta manera, podemos crear fácilmente una sóla rutina de conversión con estos datos.
En el caso de DirectX, como resultado de solicitar una descripción de la DDSURFACE, obtendremos 4 máscaras de 32 bits (RedMask, GreenMask, etc), que nos permitirán identificar el modo de display activo.
Por ejemplo, para 565RGB se devuelve en la redmask el valor 0xf800. Este valor pasado a binario es 1111100000000000b, que como puede verse corresponde a la posición de la componente RED en el color. Greenmask (también devuelta) tomaría el valor 0000011111100000b. De esta manera podemos identificar el tipo de buffer de vídeo a partir de las posiciones (xxSize) y de los valores (xxPosition).
Con toda esta información, para convertir la imagen, por ejemplo, de 15bpp a 24bpp, podemos utilizar un bucle como el siguiente :
for( y=0; y<altura; y++ ) for( x=0; x<anchura; x++ ) { color = GetPixel(x, y, origen); color = 32To24bpp( 15To32bpp( color ) ) ; PutPixel(x, y, color, destino) ; }
Así, para un redmask de 0x7c00 (0111110000000000), encontramos que el bit menor a 1 es el nº 10, y el más alto es el 14. De esta manera, el nº de bits para esta componente (xxSize) es 14-10+1=5, y la posición base (xxBase) para el desplazamiento << es el la del bit menor (10).
Con este sístema podemos, de una manera general, controlar cualquier tipo de organización de videomemoria, haciendo conversiones con estos datos. Al obtenerse RedSize y RedBase en DirectX nos encontramos en el mismo caso que en Vesa 2.0 (donde teníamos RedMaskSize y RedFieldPosition, con exactamente el mismo significado), de manera que podemos realizar el mismo tipo de conversión abstracta.
Al escribir en la dirección devuelta por el Lock() (*lpSurface), no podemos utilizar el ancho lógico de pantalla, sino que hay que usar el valor especificado por DirectX como ancho de la Surface (ya que no es obligatoriamente el ancho que le pedimos):
// usar previamente Surface->lock() PutPixelDX( x, y, color ) { char *Vram = (char *) surface->lpSurface ; dword pitch = surface->lPitch ; Vram[ (y*pitch)+x ] = color ; }
En la próxima entrega se comentarán la rutina de BestMatch() para cuantización de color, así como la aplicación de filtros a imágenes (Softens, Blurs...). La rutina de BestMatch servirá también para crear efectos de luces con LookUpTables bajo 8bpp.
Pulse aquí para bajarse los ejemplos y listados del artículo (40 Kb).
Figura 1: "Diferentes formatos de pixel."
Figura 2: "Tamaños del buffer de vídeo."
Santiago Romero