Versión 3.0

800x600 mínimo
En esta lección:

Los servicios del BIOS
-----
Modalidades de video
-----
Graficando pixeles
-----
Graficando líneas
-----
Graficando polígonos
-----
Descargas
-----


Otras secciones:

Conceptos básicos
-----
Programando en C
-----
Programando en C++
-----
Programando Windows 9x.
-----
Teoría electrónica
-----
Circuitos electrónicos
-----
Actividades adicionales
-----
Hipervínculos
-----


Contácteme:

Dudas y comentarios
-----


Programando gráficos I

     Sin duda alguna, el tema de la programación de gráficos es uno de los más solicitados a juzgar por los mensajes que amablemente me han hecho llegar a mi buzón electrónico, tan vasto es el tema que bién podríamos reunir material para todo un sitio dedicado exclusivamente a gráficos, por ésta razón, el material aquí presentado considérelo como en constante crecimiento, además no será posible cubrir todo en una sóla sección, en éste caso "Programando en C", ya que también considero necesario cubrir lo referente a la programación de gráficos para Windows 9x, tema que encontrará en la sección Programando Windows 9x. En su primera revisión, éste artículo se refiere a la programación de la tarjeta controladora de video, particularmente a la de tipo VGA, posteriormente incluiré el material referente al tipo SVGA así como el estándar VESA.

Los servicios del BIOS 

     En términos generales, cuando hablamos de programar gráficos nos estamos refiriendo a las diferentes técnicas que podemos utilizar para desplegar en pantalla información diferente al texto plano, incluyendo aquellas instrucciones que escriben directamente en las direcciones de memoria que corresponden a la memoria RAM de la tarjeta controladora de video de la computadora. Ésta afirmación no se cumple necesariamente en la programación para Windows ya que en éste ambiente incluso el texto se considera como gráfico.
     En forma normal es el BIOS de la computadora quién se encarga del acceso al hardware conectado en nuestra computadora, ésto incluye por ejemplo, las unidades de disco, los puertos y de particular interés para éste artículo, la tarjeta controladora de video, que actúa como interfaz entre la computadora y el monitor. Lo primero que debemos tener en cuenta entonces, son los servicios del BIOS, que son rutinas de software que nos sirven para tener acceso a diferentes secciones de nuestra PC, para utilizar una de éstas rutinas debemos generar una interrupción al BIOS apropiada, de acuerdo a la siguiente lista:

  • 5h Operaciones de impresión en pantalla.
  • 10h Servicios para despliegue de video.
  • 11h Determina el equipo instalado.
  • 12h Determina el tamaño de la memoria.
  • 13h Servcios para unidades de disco.
  • 14h Servicios de E/S serial
  • 14h Servicios misceláneos.
  • 16h Servicios para el teclado.
  • 17h Servicios para impresoras.
  • 18h Acceso al lenguaje BASIC.
  • 19h Reinicializar la PC.
  • 1Ah Servicios de reloj en tiempo real.

     Observando la lista nos damos cuenta que para tener acceso a los servicios relacionados con el despliegue de video debemos generar la interrupción al BIOS 10h, ésto implica pasar diversos valores a los registros ax, bx, cx, dx, lx y es:bp. En éste artículo no daré una explicación de los diferentes registros pues ésta información la encontrará en la sección de Conceptos básicos. Además, si desea amplia información respecto a las interrupciones del BIOS puede consultar la página de Ralf Brown, es en verdad un "regalo para los programadores en DOS".

Volver al principio

Hola mundo

Modalidades de video 

     En la actualidad, prácticamente todas las tarjetas adaptadoras de video son de tipo VGA, siglas que significan Video Graphics Array, aunque no tardaron en rebautizar el término por éste otro: Video Graphics Adaptor, de cualquier forma nos estamos refiriendo a la tarjeta controladora de video de la PC. Existen diferentes tipos de adaptadores de video, monocromos, de color de mediana resolución (CGA y MCGA), y de color de alta resolución (EGA y VGA). Para cada uno de los adaptadores de video existen diferentes modalidades de video que se utilizan para desplegar sea texto ó gráficos, ésta es la lista:

Modo Resolución Tipo Adaptador
0h 40 x 25 B. y N. Alfanumérico CGA/EGA/VGA
1h 40 x 25 Color Alfanumérico CGA/EGA/VGA
2h 80 x 25 B. y N. Alfanumérico CGA/EGA/VGA
3h 80 x 25 Color Alfanumérico CGA/EGA/VGA
4h 320 x 200 Color Gráfico CGA/EGA/VGA
5h 320 x 200 B. y N. Gráfico CGA/EGA/VGA
6h 640 x 200 B. y N. Gráfico CGA/EGA/VGA
7h 80 x 25 B. y N. Alfanumérico EGA/VGA (mono)
13h 320 x 200 Color Gráfico EGA/VGA
14h 640 x 200 Color Gráfico EGA/VGA
15h 640 x 350 Color Gráfico EGA/VGA (mono)
16h 640 x 350 Color Gráfico EGA/VGA

     Como podemos observar, es posible utilizar todas las modalidades de video si contamos con un adaptador de tipo VGA. Para el propósito de éste artículo, interesa en particular el modo de video 13h en el cual la pantalla tiene una resolución de 320 pixeles de ancho por una altura de 200 pixeles de tal manera que al establecer un sistema de coordenadas, el punto (0,0) está ubicado en la esquina superior izquierda de la pantalla del monitor, siendo el extremo opuesto de la pantalla el correspondiente a la coordenada (319, 199), (Fig. 1). Como ya se dijo, la resolución es de 320 x 200 pixeles en tanto que la capacidad de color es de 256 colores diferentes, es decir, podemos representar cada uno de los diferentes colores utilizando un byte de memoria. Como la resolución demanda una cantidad igual a 320x200=64000 pixeles, necesitamos por lo tanto 64000 bytes de memoria RAM de video.

Modo de video 13

Volver al principio

Hola mundo

Graficando pixeles 

     Para desplegar gráficos en la pantalla del monitor necesitamos colocar el modo de video a un valor igual a 13h, para ésto utilizamos la interrupción 10h del BIOS especificando el valor 0x00 en el registro ax y el modo de video deseado en el registro al (0x13h) como puede verse en las líneas 17 a la 19 del siguiente programa, llamado grafico1.c, que despliega 250,000 pixeles aleatoriamente.


/**********************************************************
*  grafico1.c                                             *
*  trazado de pixeles utilizando el BIOS                  *
*  (c)1999, Jaime Virgilio Gómez Negrete                  *
**********************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>

int main()
{
    int x, y, color;
    long i;
    union REGS pixel;

    /* Modo de video 13 */
    pixel.h.ah = 0x00;
    pixel.h.al = 0x13;
    int86(0x10, &pixel, &pixel);

    for(i=0; i<250000; i++)
    {
        x = rand()%320;
        y = rand()%200;
        color = rand()%256;

        pixel.h.ah = 0x0C; /* funcion para imprimir un pixel */
        pixel.h.al = color;
        pixel.x.cx = x;
        pixel.x.dx = y;
        int86(0x10, &pixel, &pixel);
    }

    /* retorna a modo de video 3 */
    pixel.h.ah = 0x00;
    pixel.h.al = 0x03;
    int86(0x10, &pixel, &pixel);

    return 0;
}

     Después de especificar el modo de video deseado, en éste caso, el modo 13h, utilizamos un bucle para graficar los pixeles. La forma más sencilla es utilizar la función del BIOS 0x0C especificandola en el registro ah. Para utilizar ésta función especificamos la coordenada x en el registro cx y la coordenada y en el registro dx, mientras que el valor correspondiente al color lo especificamos en el registro al. Consulte las líneas 27 a la 31 del programa. Como se puede apreciar, trazar pixeles utilizando el BIOS es relativamente sencillo, pero como al interés de programar gráficos está implícito el de la velocidad, pues ésta técnica resulta insuficiente, como veremos en el siguiente párrafo, existen alternativas...

Escribiendo en la memoria RAM de video

     Como ya se mencionó, la memoria necesaria para trabajar en el modo gráfico 13h es de 64000 bytes. Si checamos las propiedades de la PC en la sección correspondiente a la memoria, podemos ver que la memoria de video está localizada en el segmento 0xA000h, por lo tanto, escribiendo en éste segmento de memoria estaremos a su vez escribiendo en la pantalla del monitor, el color desplegado depende del valor escrito en memoria. La memoria de video es lineal lo que implica utilizar un mecanismo especial para especificar los valores correspondientes a las coordenadas x e y: tomamos en primer lugar el valor de la coordenada y y la multiplicamos por el ancho de la pantalla (320), a éste resultado le sumamos el valor de la coordenada x y así obtenemos un valor de offset, que en español sería algo así como un valor de compensación, pero para entendernos mejor con el lenguaje C, utilizaremos la palabra inglesa offset.
     Necesitamos también declarar un puntero de tipo unsigned char que señale al segmento de memoria de video 0xA000h y finalmente, utilizando la función clock() podemos escribir un programa que nos sirva para comparar el tiempo que tomaría graficar 256,000 pixeles aleatoriamente, utilizando en primer lugar la interrupción del BIOS y luego escribiendo directamente en la memoria RAM de video:


/**********************************************************
*  grafico2.c                                             *
*  Este programa compara la velocidad para graficar       *
*  utilizando el BIOS y la memoria RAM de video           *
*  (c)1999, Jaime Virgilio Gómez Negrete                  *
**********************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <time.h>

/* puntero a la memoria de video */
unsigned char *VGA=(unsigned char *)0xA0000000L;

int main()
{
    int         x, y, color;
    float       t1, t2;
    long        i;
    union REGS  pixel;
    clock_t     reloj, reloj2;

    /* colocar el modo de video 13h */
    pixel.h.ah = 0x00;
    pixel.h.al = 0x13;
    int86(0x10, &pixel, &pixel);

    reloj = clock();  /* registra el momento de inicio */
    /* grafica 256000 pixeles utilizando el BIOS */
    for(i=0; i<256000; i++)
    {
        pixel.h.ah = 0x0C;
        pixel.h.al = rand()%256;
        pixel.x.cx = rand()%320;
        pixel.x.dx = rand()%200;
        int86(0x10, &pixel, &pixel);
    }
    reloj2 = clock();  /* registra el momento final */
    t1 = ((float)reloj2-(float)reloj)/CLOCKS_PER_SEC;

    /* se invoca de nuevo el modo 13h para limpiar la pantalla */
    pixel.h.ah = 0x00;
    pixel.h.al = 0x13;
    int86(0x10, &pixel, &pixel);

    reloj = clock();/* registra el momento de inicio */
    /* grafica 256000 pixeles utilizando la memoria de video */
    for(i=0;i<256000;i++)
    {
        x = rand()%320;
        y = rand()%200;
        color = rand()%256;
        VGA[y*320+x] = color;
        /* VGA[(y<<8)+(y<<6)+x] = color; */
    }
    reloj2 = clock();
    t2 = ((float)reloj2-(float)reloj)/CLOCKS_PER_SEC;

    /* retornamos al modo 3h para desplegar texto */
    pixel.h.ah = 0x00;
    pixel.h.al = 0x03;
    int86(0x10, &pixel, &pixel);

    /* desplegamos resultados */
    printf("Graficar con el BIOS tomo %f segundos.\n", t1);
    printf("Graficar en memoria tomo %f segundos.\n", t2);
    printf("Graficar en memoria fue %f veces mas rapido.\n", t1/t2);

    return 0;
}

     Este programa es similar a grafico1.c excepto que lo he modificado para que nos permita medir el tiempo que toma graficar 256,000 pixeles (4 veces la resolución del modo de video 13h) utilizando en primer lugar las funciones del BIOS y posteriormente, escribiendo directamente en la memoria RAM de video, de particular interés es el bucle que nos sirve para éste propósito, líneas 49 a la 56, en la línea 54 podemos apreciar que se asigna a un array llamado VGA[ ] el valor del color con que se graficará el pixel en la posición de pantalla especificada por el valor del offset indicado entre los corchetes del array, de acuerdo a lo explicado en el párrafo anterior. En la línea 55 se encuentra entre comentarios una forma alterna para calcular el offset que hace uso del desplazamiento a la izquierda de bits, tomando un número n cualquiera y desplazando sus bits una posición a la izquierda es el mismo efecto que multiplicar ése número n por 2. En general, si un número n lo desplazamos x espacios a la izquierda el resultado es 2xn. En el caso concreto del valor de y que es de 320, como no es múltiplo de 2, lo que hacemos es factorizar 320 en partes que sean múltiplos de 2, o sea, 256 y 64.
     Es importante hacer notar que éstos programas se deben compilar utilizando un modelo de memoria mediano ó grande, recuerde que el modo de video 13h requiere 64,000 bytes por lo que el modelo de memoria pequeño (small) es insuficiente para correr éstos programas.

Volver al principio

Hola mundo

Graficando líneas 

     En éste momento ya sabemos cómo graficar pixeles, pues bién, para desplegar una línea recta el procedimiento no cambia sustancialmente, de hecho hacemos exactamente lo mismo, colocamos una serie de pixeles, alineados de acuerdo a la ecuación de la línea recta. Sabemos que dos puntos son suficientes para definir una línea recta, refiriendonos a la imagen que sigue podemos ver una línea recta (en rojo) que parte del punto P1 de coordenadas (X1, Y1) y termina en el punto P2 de coordenadas (X2, Y2). En la imagen podemos ver en color gris el área que ocuparía la pantalla del monitor durante la modalidad de video 13h. Recuerde que la coordenada del origen (0, 0) se encuentra en la esquina superior izquierda de la pantalla del monitor.

Cómo graficar una recta

     Para graficar una línea necesitamos en primer lugar calcular la pendiente de la recta, para ésto utilizamos la forma de la pendiente y un punto de la ecuación de una recta, que es la ecuación de la misma cuando se conoce un punto P(x1, y1) en la recta y su pendiente, m.

     y = m (x - x1) + y1

     Por otra parte, al conocer los dos puntos que definen la línea recta, podemos conocer la pendiente m de la misma, utilizando la siguiente ecuación:

     m = (y2 - y1) / (x2 - x1)

     Como ya se dijo, la coordenada del origen se encuentra en la esquina superior izquierda de la pantalla del monitor, esto implica que no podemos manejar un sistema cartesiano de coordenadas, de tal manera que debemos implementar un mecanismo que nos indique si la recta a trazar es más horizontal que vertical ó visceversa, ésta información nos la proporciona el signo del valor de las distancias horizontal y vertical, para ésto utilizamos una macro definida en la línea 13 del siguiente programa:


/**********************************************************
*   grafico3.c                                            *
*   dibuja una línea recta                                *
*   (c)1999, Jaime Virgilio Gómez Negrete                 *
**********************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <time.h>

/* si x=0 entonces x=0; si x<0 entonces x = -1; si x>0 entonces x=1 */
#define signo(x) ((x<0)?-1:((x>0)?1:0))

int main()
{
    int x1=0, x2=319, y1=0, y2=199, dx, dy, dxabs, dyabs, sdx, sdy, i;
    union REGS linea;
    float pendiente;
    clock_t reloj1, reloj2;

    /* Modo de video 13h */
    linea.h.ah = 0x00;
    linea.h.al = 0x13;
    int86(0x10, &linea, &linea);

    dx = x2-x1; /* distancia horizontal */
    dy = y2-y1; /* distancia vertical */
    dxabs = abs(dx);
    dyabs = abs(dy);
    sdx = signo(dx);
    sdy = signo(dy);

    if(dxabs>=dyabs)
    {
        pendiente = (float)dy / (float)dx;
        for (i=0; i!=dx; i+=sdx)
        {
            reloj1 = clock();
            linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
            linea.h.al = 2; /* color verde */
            linea.x.cx = i+x1;
            linea.x.dx = (pendiente*i)+y1;
            int86(0x10, &linea, &linea);
            do
            reloj2 = clock();
            while((reloj2-reloj1)<25);
        }
    }
    else
    {
        pendiente = (float)dx / (float)dy;
        for (i=0; i!=dy; i+=sdy)
        {
            reloj1 = clock();
            linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
            linea.h.al = 4; /* color rojo */
            linea.x.cx = (pendiente*i)+x1;
            linea.x.dx = i+y1;
            int86(0x10, &linea, &linea);
            do
            reloj2 = clock();
            while((reloj2-reloj1)<25);
        }
    }

    /* implementa un retardo de tiempo de 2 segundos */
    reloj1 = clock();
    do
    reloj2 = clock();
    while((reloj2-reloj1)<2000);

    /* retorna a modo de video 3 */
    linea.h.ah = 0x00;
    linea.h.al = 0x03;
    int86(0x10, &linea, &linea);

    return 0;
}

    En palabras sencillas, la macro signo devuelve cero si x es igual a cero, -1 si x es menor que cero y 1 si x es mayor que cero. Ésta información la utilizaremos en los bucles que sirven para graficar, pixel por pixel, la recta definida por los puntos cuyas coordenadas están definidas en los valores de las variables x1, y1 y x2, y2. En las líneas 27 y 28 se calculan las respectivas distancias horizontal y vertical para posteriormente calcular la pendiente de la recta en las líneas 36 y 52. Dependiendo si la recta es más horizontal que vertical ó visceversa se utiliza uno de los dos bucles para graficar la línea recta en la pantalla del monitor, si la recta es más horizontal entonces se graficará en color verde tal y como se especifica en la línea 41, por otra parte, si la recta es más vertical ésta se graficará en color rojo, de acuerdo a la instrucción dictada en la línea 57.

     El programa grafico3.c incluye unos bucles do-while para inducir un retardo de tiempo tal que nos permita observar la construcción de la línea recta y demostrar así que ésta se dibuja pixel por pixel. Al final del programa observamos otro bucle do-while que implementa un retardo de dos segundos que nos permite ver por un momento la línea recta completa. Los mencionados bucles do-while utilizan la función clock( ). Conviene estudiar detenidamente éste programa y experimentar con diferentes valores para las variables que definen los puntos de la recta.

Volver al principio

Hola mundo

Graficando polígonos 

     De acuerdo a lo expuesto en las secciones previas, concluimos que dibujar un polígono se reduce a lo siguiente, excepto por el círculo, el resto de los polígonos los podemos dibujar simplemente trazando una serie de líneas rectas, ya vimos que una línea recta a su vez la trazamos dibujando pixel por pixel, situación que también se aplica a los círculos. En la forma más general posible, decimos que se puede trazar cualquier tipo de figura geométrica, incluyendo aquellas que simulan tercera dimensión, simplemente graficando una serie de pixeles de acuerdo a un algoritmo determinado. En la programación de gráficos la velocidad juega un papel de primera importancia y es por lo general uno de los objetivos a cumplir al desarrollar nuestro algoritmo para gráficos.

     El siguiente programa, grafico4.c demuestra cómo dibujar un rectángulo, el cual está formado por cuatro líneas rectas, cada una de ellas a su vez se grafica trazando pixel por pixel de acuerdo a las condiciones impuestas en su respectivo bucle.


/**********************************************************
*   grafico4.c                                            *
*   dibuja polígonos                                      *
*   (c)1999, Jaime Virgilio Gómez Negrete                 *
**********************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <time.h>

int main()
{
    int x1=50, y1=50, x2=269, y2=149, dx, dy, dxabs, dyabs, i;
    union REGS linea;
    clock_t reloj1, reloj2;

    /* Modo de video 13h */
    linea.h.ah = 0x00;
    linea.h.al = 0x13;
    int86(0x10, &linea, &linea);

    dx = x2-x1; /* distancia horizontal */
    dy = y2-y1; /* distancia vertical */
    dxabs = abs(dx);
    dyabs = abs(dy);

    if(dxabs>=dyabs)
    {
        for(i=0; i<dxabs; i++)
        {
            reloj1 = clock();
            linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
            linea.h.al = 2; /* color verde */
            linea.x.cx = i+x1;
            linea.x.dx = y1;
            int86(0x10, &linea, &linea);
            do
            reloj2 = clock();
            while((reloj2-reloj1)<25);
        }
        for(i=0; i<dyabs; i++)
        {
            reloj1 = clock();
            linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
            linea.h.al = 2; /* color verde */
            linea.x.cx = x2;
            linea.x.dx = i+y1;
            int86(0x10, &linea, &linea);
            do
            reloj2 = clock();
            while((reloj2-reloj1)<25);
        }
        for(i=dxabs; i>0; i--)
        {
            reloj1 = clock();
            linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
            linea.h.al = 2; /* color verde */
            linea.x.cx = i+x1;
            linea.x.dx = y2;
            int86(0x10, &linea, &linea);
            do
            reloj2 = clock();
            while((reloj2-reloj1)<25);
        }
        for(i=dyabs; i>0; i--)
        {
            reloj1 = clock();
            linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
            linea.h.al = 2; /* color verde */
            linea.x.cx = x1;
            linea.x.dx = i+y1;
            int86(0x10, &linea, &linea);
            do
            reloj2 = clock();
            while((reloj2-reloj1)<25);
        }
    }
    else
    {
        for(i=0; i<dxabs; i++)
        {
            reloj1 = clock();
            linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
            linea.h.al = 4; /* color rojo */
            linea.x.cx = i+x1;
            linea.x.dx = y1;
            int86(0x10, &linea, &linea);
            do
            reloj2 = clock();
            while((reloj2-reloj1)<25);
        }
        for(i=0; i<dyabs; i++)
        {
            reloj1 = clock();
            linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
            linea.h.al = 4; /* color rojo */
            linea.x.cx = x2;
            linea.x.dx = i+y1;
            int86(0x10, &linea, &linea);
            do
            reloj2 = clock();
            while((reloj2-reloj1)<25);
        }
        for(i=dxabs; i>0; i--)
        {
            reloj1 = clock();
            linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
            linea.h.al = 4; /* color rojo */
            linea.x.cx = i+x1;
            linea.x.dx = y2;
            int86(0x10, &linea, &linea);
            do
            reloj2 = clock();
            while((reloj2-reloj1)<25);
        }
        for(i=dyabs; i>0; i--)
        {
            reloj1 = clock();
            linea.h.ah = 0x0C; /* funcion para imprimir un pixel */
            linea.h.al = 4; /* color rojo */
            linea.x.cx = x1;
            linea.x.dx = i+y1;
            int86(0x10, &linea, &linea);
            do
            reloj2 = clock();
            while((reloj2-reloj1)<25);
        }
    }

    /* implementa un retardo de tiempo de 2 segundos */
    reloj1 = clock();
    do
    reloj2 = clock();
    while((reloj2-reloj1)<2000);

    /* retorna a modo de video 3 */
    linea.h.ah = 0x00;
    linea.h.al = 0x03;
    int86(0x10, &linea, &linea);

    return 0;
}

     Como se vé, el programa es similar a grafico3.c, excepto que en lugar de dibujar una sola línea, se grafican cuatro, en color verde si el rectángulo es más horizontal que vertical, ó bién, en color rojo si las coordenadas especificadas en las variables x1, y1, x2, y y2 determinan que la figura es más vertical que horizontal. Obviamente el programa grafico4.c no demuestra velocidad sino el hecho de que los gráficos se construyen a partir de pixeles individuales. El estudiante observador no tendrá dificultad para combinar lo expuesto en los programas de ésta lección y lograr una rutina de dibujo tan rápida como se lo permita su respectiva computadora.

Volver al principio

Hola mundo

Descargas 

Descargue el código fuente de éste capítulo. Archivo comprimido grafico1.zip, 2.9 Kb.

Volver al principio

Hola mundo

© 1999 Virgilio Gómez Negrete, Derechos Reservados