Autor: Gabo Inc 1. Introducción Las primitivas de dibujo son los elementos básicos de los que disponemos para realizar cualquier representación gráfica: puntos, líneas, círculos, … La primera de estas primitivas es muy dependiente del hardware de la máquina, del sistema operativo, del modo de vídeo, … y dado que siempre podemos usar la función para este fin que viene en alguna librería de gráficos, vamos a tomarla como básica-disponible, es decir, cuando queramos dibujar algo, denotaremos PIXEL(x,y,color) a una función que vamos a tener a nuestra disposición siempre que la necesitemos.

La sintaxis de esta función en las distintas librerías no es muy distinta de la que adoptamos aquí como convenio. Así, a partir de ahora, en nuestros algoritmos, PIXEL(x,y,color) significa “pintar un punto en la posición (x,y) de la pantalla del color ‘color’”. Hay que hacer notar que en la pantalla, la esquina superior izquierda tiene coordenadas (0, 0), y que la coordenada x crece hacia la derecha mientras que la coordenada y crece hacia abajo. En la resolución en la que trabajemos, tendremos un máximo de XMAX puntos de ancho por YMAX puntos de alto, por tanto, el rango permitido para dibujar es el rectángulo de esquina superior izquierda (0, 0) y de esquina inferior derecha (XMAX-1, YMAX-1).

En este capítulo de introducción vamos a estudiar cómo usar una librería genérica de dibujo, dedicaremos algunos anexos al uso de librerías concretas, y cuando ya tengamos soltura en su manejo, veremos, a partir del capítulo siguiente, el estudio de algoritmos para dibujar las primitivas que usemos de estas librerías.

Una estrategia sencilla de programación gráfica podría esquematizarse en el siguiente gráfico:

Vamos a explicar cada una de estas etapas:

Detección del hardware: Trata de averiguar qué tipo de máquina se usa y qué características posee, pues esto nos determina qué modos gráficos podemos usar.

Por modo gráfico se entiende una configuración que nos informa de si es modo texto o gráfico, qué resolución tiene, si es paleta o true-color, y cuántos colores podemos usar.

Inicialización del modo gráfico: En esta fase escogemos las características del modo concreto que queramos usar. En modo texto tendremos como posibles resoluciones 80×25×16, 132×43×16,… y en modo gráfico tenemos 320×200×16, 320×200×256, 320×200×64K, 640×480×16, 800×600×16M, … (hay muchas más posibilidades). Siempre los modos de 256 o menos colores son modos de paleta, y los modos de 32K o más colores, son modos true-color.

Según la librería, la función de inicialización de modo de vídeo es distinta. Suele usarse un identificador para cada modo distinto. Cuando no hablemos de una librería concreta, escribiremos:

Inicializar_Modo(Identificador)

y como ‘Identificador’ escribiremos una constante que empieza por m y, a continuación, el modo concreto siguiendo la notación MM Mx NN Nx CC?, donde MMM es la resolución horizontal, NNN es la resolución vertical y CC es el número de colores. Por ejemplo, usaremos identificadores como:

m320×200×256

m640×480×16

m80×25×16

m800×600×32768

Así, en nuestra notación, si queremos inicializar el modo 640×400×256, escribiremos:

Inicializar_Modo(m640×400×256)

Una vez inicializado el modo, tenemos dos posibilidades: modificar el contexto gráfico o usar alguna función de dibujo. Se entiende por contexto gráfico un conjunto de propiedades que determinan con qué estilo se van a pintar las primitivas en pantalla.

Estilo son una serie de opciones que se aplican cuando tiene sentido aplicarse (ahora vemos qué quiere decir esto).

El estilo más básico es el color, que es aplicable a todas las primitivas, pues todas ellas son susceptibles de poder ser dibujadas con algún color.

También podemos establecer la forma en que las líneas serán dibujadas: grosor y trazo continuo o discontinuo, pudiendo elegir entre varias posibilidades en este último. El estilo de trazo de líneas pone de manifiesto que cualquier estilo no es susceptible de ser aplicable a cualquier primitiva: por ejemplo, no tiene sentido aplicar un estilo de trazo de líneas a un punto. De todas formas, no hay que preocuparse por qué primitivas admiten un estilo y qué primitivas no lo admiten: el esquema general de una librería gráfica en este aspecto es, primero, fijar el estilo, para después dibujar la primitiva con ese estilo, si es posible aplicarlo, y si no es posible, la dibuja sin aplicarlo, únicamente empleará aquello aplicable a la primitiva en cuestión.

Como decíamos antes, además de tener grosores distintos, las líneas pueden tener varios tipos de trazo. A continuación mostramos unos ejemplos de trazos y los identificadores que usaremos (pues luego las librerías gráficas dan completa información y ejemplos sobre las posibilidades):

El último estilo que vamos a ver es aplicable a figuras que pueden rellenarse, y se conoce como patrón de relleno. Posibles patrones de relleno (y sus identificadores para nosotros) son, por ejemplo, SOLIDO, que hace referencia al relleno normal, TRAMADO, que hace una trama de líneas horizontales y verticales en lugar del relleno sólido, TRAMADO_OBL, que hace una trama de líneas oblicuas, PUNTEADO, que hace una trama de puntos, TRANSPARENTE, que no rellenará la figura, etc…

Ahora que hemos estudiado los posibles estilos, vamos a ver la notación que vamos a usar para las funciones que cambian los estilos:

   Cambiar_Color(ID_COLOR);
   Cambiar_Trazo_Linea(ID_LINEA,ID_GROSOR);
   Cambiar_Patron_Relleno(ID_RELLENO);

Notar lo siguiente: si pintamos un rectángulo, su borde se pintará con el estilo del trazo de líneas, mientras que su interior se pintará con el estilo del patrón de relleno. Si no especificamos estilo alguno, las librerías toman por defecto los siguientes valores:

   ID_COLOR es el identificativo del color negro.
   ID_LINEA es el identificativo del trazo continuo.
   GROSOR es el trazo identificativo de 1 píxel de grosor.
   ID_RELLENO es el identificativo del relleno sólido.

Estudiadas las posibilidades que tenemos para dibujar primitivas, entramos ya en las primitivas que tenemos disponibles:

Pixel(x,y,color);

Pinta un pixel en la posición (x,y) de la pantalla, del color especificado en color. Es la única primitiva a la que no se le aplica estilo alguno.

Linea(x0,y0,x1,y1);

Pinta una línea que va del punto (x0,y0) al punto (x1,y1) con el estilo que se haya definido. Si no hay estilos definidos, se aplica el estilo por defecto.

Rectangulo(x0,y0,x1,y1);

Pinta un rectángulo cuya esquina superior izquierda tiene por coordenadas (x0,y0) y cuya esquina inferior derecha tiene por coordenadas (x1,y1). El trazo de las cuatro líneas será el que venga dado por el estilo de línea. Si no hay estilo de línea definido, se usará el estilo por defecto, trazo continuo.

Rectangulo R(x0,y0,x1,y1);

Igual que Rectangulo, pero la figura se rellenará con el estilo de relleno que haya definido en ese momento.

Circulo(x,y,radio);

Pinta un círculo de centro (x,y) y radio ‘radio’. El valor del radio debe ser un número positivo para que tenga sentido. El círculo se pinta con el estilo que haya definido en ese momento o con el estilo por defecto si no se han definido estilos.

Circulo R?(x,y,radio);

Igual que Circulo, pero además se rellenará con el estilo de relleno que haya definido en ese momento.

Elipse(x,y,a,b);

Pinta una elipse de centro (x,y) y semiejes mayor y menor a y b, respectivamente. Ambos deben ser números positivos para que tenga sentido. La elipse se pinta con el estilo definido en ese momento o con el estilo or defecto si no hay ninguno definido.

Y muchas otras…

Con esto finalizamos este capítulo dedicado a una introducción de qué nos encontramos cuando nos planteamos una aplicación que requiera gráficos. En el siguiente capítulo pasamos a estudiar cómo se dibuja una de las primitivas más elementales, el segmento que une dos puntos. En los apéndices podremos ver implementaciones concretas de librerías gráficas concretas, donde se explican las funciones más importantes para poder hacer gráficos.

2. Trazado de líneas Pintar una línea no es una tarea tan trivial como pueda parecer en principio, y mucho menos pintar una línea de forma eficiente. Si lo hacemos a mano, todo es muy sencillo: pintamos dos puntos y, a continuación, trazamos, con ayuda de una regla, el segmento que los une. La idea de dibujar una línea en la pantalla es igualmente sencilla, se trata, sin más, de pintar los extremos y luego pintar el segmento que los une, pero, ¿cómo pintamos este segmento? Es decir, ¿de qué manera podemos saber qué puntos pertenecen a ese segmento y qué puntos no?, ¿cómo calculamos los puntos que pertenecen al segmento que une los extremos?

Para ello, necesitaremos un poquito de matemáticas, en concreto, de geometría analítica del plano. Pero calma, por muy sofisticado que pueda sonar el nombre, los conceptos no son en absoluto complicados.

Comenzamos recordando cómo se tiene la llamada ecuación vectorial de la recta en el plano: si tenemos dos puntos P=(x_0,y_0) y Q=(x_1, y_1) de la recta, v=(x_1-x_0,y_1-y_0) es vector director de la recta, y dicha ecuación es:

         (x, y) = (x_0, y_0) + A*(x_1-x_0, y_1-y_0)

Si operamos un poco, llegamos a las ecuaciones paramétricas:

         x = x_0 + A*(x_1-x_0)
         y = y_0 + A*(y_1-y_0)

y de aquí, despejando e igualando A=A, SUPONIENDO x_1-x_0 <> 0 E y_1-y_0 <> 0 (<> quiere decir distinto de):

           x - x_0       y - y_0
         ----------- = -----------
          x_1 - x_0     y_1 - y_0

Veamos qué sucede con los casos críticos:

Si x_1-x_0 = 0 pero y_1-y_0 <> 0

En este caso, lo que tenemos es que el extremo horizontal de la recta se mantiene fijo, y sólo se mueve el vertical, es decir, tenemos una línea vertical.

Si y_1-y_0 = 0 pero x_1-x_0 <> 0

En este caso, tenemos que el extremo vertical de la recta se mantiene fijo, y sólo se mueve el horizontal, es decir, tenemos una línea horizontal.

Si x_1-x_0 = 0 E y_1-y_0 = 0

En este caso, no se mueve ninguno de los extremos, estamos ante el caso de una recta degenerada, es decir, un punto.

Así que, salvados estos casos críticos, vamos a seguir haciendo operaciones en nuestra ecuación de la recta, suponiendo que ya no tenemos estos casos (es decir, que ya tenemos x_1-x_0<>0 E y_1-y_0<>0).

Retomando la ecuación

           x - x_0       y - y_0
         ----------- = -----------
          x_1 - x_0     y_1 - y_0

Si despejamos y, se tiene la ecuación y = m*x + n, donde

          y_1 - y_0
     m = -----------   , n = -m*x0 + y0
          x_1 - x_0

Con esta ecuación (conocida como ecuación explícita de la recta), vamos ya a estudiar el primer algoritmo de dibujo de líneas, que es el algoritmo incremental básico:

Llamamos dx = x_1-x_0, dy = y_1-y_0. Calculamos m = dy/dx, y vamos a suponer que |m|<=1.

1. Comenzamos en el extremo izquierdo (x0<x1) e incrementamos x de 1 en 1.

2. Calculamos y como y_i = m*x_i +n para cada x_i

Notar que este cálculo puede hacerse un poco más eficiente si tenemos en cuenta lo siguiente:

y_{i+1} = m*x_{i+1} + n = m*(x_i + dx) + n = (m*x_i + n) + m*dx = y_i + m*dx = (dx = 1) = y_i + m

3. PIXEL(x_i,REDONDEO(y_i),color)

Si |m|›1, cambiamos los papeles de x, y, pintando con incremento 1/m en x_i

Este algoritmo presenta algunos inconvenientes, como que realiza todas las operaciones con números reales (más tiempo de cómputo, posibles pérdidas de precisión). Para resolver este problema, vamos a ver un método que se basa en el cálculo de la distancia de la recta real a los píxels de la pantalla para escoger qué pixel pintar, con lo que evitaremos las operaciones con reales para hacerlas únicamente con enteros, cosa que acelerará bastante el proceso de dibujo.

Algoritmo del Punto Medio

Comenzaremos suponiendo que 0<m<1, y queremos trazar la línea que va de (x_0,y_0) a (x_1,y_1). Supongamos que tenemos pintado un punto cualquiera de la recta, llamémosle P=(x_p,y_p). Tal y como vemos en el dibujo, tenemos dos posibilidades a la hora de elegir el punto siguiente a pintar:

Llamamos A = (x_p + 1, y_p), B = (x_p + 1, y_p + 1) a esos dos puntos posibles (notar que, en coordenadas de pantalla, las coordenadas de B serán (x_p + 1, y_p - 1)), y llamamos Q al punto de la recta real que intersecta con la recta x = x_p + 1. Vamos a ver qué sucede con M, el punto medio entre A y B, cuyas coordenadas son:

           (                 1  )
       M = ( x_p + 1, y_p + --- )
           (                 2  )

Si tenemos que M está por debajo de Q, pintaremos B, y, en caso contrario, pintaremos A. Por tanto, tenemos que ver a qué lado de la recta real está M. Para ello, vamos a enunciar un resultado elemental de geometría que no es necesario probar:

En el plano, la ecuación implícita de cualquier recta viene dada por a*x + b*y + c = 0. Definimos la función F(x, y) = a*x + b*y + c. Entonces, si tomamos un punto (x’, y’) que esté por encima de la recta, F(x’, y’)>0; si pertenece a la recta, F(x’, y’)=0, y si está por debajo de ella, F(x’, y’)<0.

Con la notación que hemos estado siguiendo hasta ahora, teníamos dx = x_1 - x_0 y dy = y_1 - y_0. Si operamos para convertir la ecuación que teníamos de la recta en una ecuación implícita de la forma a*x + b*y + c = 0, tendremos que a = dy, b = -dx. Además, en la ecuación explícita y = m*x + n, m = a/(-b) = - a/b, por lo que, en la implícita tenemos que c = n*dx.

Luego la función F(x, y) de la que hablábamos en el resultado teórico nos queda F(x, y) = x*dy - y*dx + n*dx

Ahora, llamamos d = F(M) = F(x_p + 1,y_p + 1/2) = a*(x_p + 1) + b*(y_p + 1/2) + c. Como decíamos antes, si M está por encima de la recta, tendremos que F(M)>0, si pertenece a la recta, F(M)=0, y si está por debajo de la recta, F(M)<0. Por tanto:

· Si d>0, elegimos A como punto a dibujar

· Si d<0, elegimos B como punto a dibujar

· Si d=0, es indiferente cuál elijamos, A o B

Vamos ahora a detallar los pasos a seguir en cada caso:

Si d<0, pintamos B, y ahora M se incrementa una unidad en x, por tanto:

            (                1 )                   (       1 )
 d_vieja = F(x_p + 1, y_p + ---) = a*(x_p + 1) + b*(y_p + ---) + c
            (                2 )                   (       2 )

            (                1 )                   (       1 )
 d_nueva = F(x_p + 2, y_p + ---) = a*(x_p + 2) + b*(y_p + ---) + c
            (                2 )                   (       2 )

es decir, d_nueva = d_vieja + a = d_vieja + dy

Si d>0 pintamos A, y ahora M se incrementa una unidad en x y en y, por tanto:

            (                1 )                   (       1 )
 d_vieja = F(x_p + 1, y_p + ---) = a*(x_p + 1) + b*(y_p + ---) + c
            (                2 )                   (       2 )

            (                3 )                   (       3 )
 d_nueva = F(x_p + 2, y_p + ---) = a*(x_p + 2) + b*(y_p + ---) + c
            (                2 )                   (       2 )

es decir, d_nueva = d_vieja + a + b = d_vieja + dy - dx

Lo que vemos es que no necesitamos calcular F(M) en cada paso, sino que podemos obtenerlo a partir del valor calculado anteriormente y los valores dx, dy que tenemos precalculados desde el principio.

El valor del primer punto medio es F(x_0 + 1, y_0 + 1/2) = F(x_0, y_0) + a + b/2 = a + b/2, pues, como (x_0, y_0) pertenece a la recta, F(x_0, y_0) = 0. Ahora, como lo que realmente nos interesa es el signo y no el valor concreto de F en los puntos medios, nos da lo mismo calcular el signo de a + b/2 que el de 2*a + b, con lo que ya tenemos todas las operaciones resueltas usando únicamente números enteros.

Hay que hacer notar que este algoritmo sólo es válido cuando la pendiente de la recta, m, es un valor mayor estricto que 0 y menor estricto que 1. Es un interesante ejercicio generalizarlo a cualquier valor de la pendiente, escribiendo el algoritmo completo. Si alguien lo resuelve, puede enviármelo, y yo publicaré la solución como un capítulo más del curso.

Una pista para ello es la siguiente: si la pendiente es mayor que 1, en lugar de avanzar de x_i en x_i + 1 y calcular el valor y correspondiente, avanzaremos de y_i en y_i + 1 y calcularemos el valor de x correspondiente. Si la pendiente es negativa, los incrementos a sumar, en vez de ser positivos, serán negativos. Y ya no digo más ;-)

3. Dibujo de polígonos Este capítulo es más breve pues, sabiendo pintar líneas, el paso siguiente es bastante simple.

Por polígono se entiende una figura cuyos elementos sobresalientes son los vértices (‘esquinas’) y las aristas, completamente cerrada y de cada vértice únicamente salen dos aristas.

Comenzamos con el caso particular de un rectángulo: los datos que necesitamos depende de cómo vayamos a implementarlo. Si pensamos en él considerando como información relevante una esquina, el ancho y el alto, lo dibujaríamos como sigue:

Supongamos que la esquina escogida es la superior izquierda, y tiene por coordenadas (x0,y0); las coordenadas de las cuatro esquinas son:

Por tanto, para dibujarlo haremos:

   Linea(x0,y0,x0+ancho,y0);
   Linea(x0,y0,x0,y0+alto);
   Linea(x0+ancho,y0,x0+ancho,y0+alto);
   Linea(x0,y0+alto,x0+ancho,y0+alto);

Ahora, si pensamos en un rectángulo considerando como información relevante dos esquinas situadas en diagonal, lo pintaríamos de la siguiente forma:

Supongamos que las esquinas elegidas son la superior izquierda y la inferior derecha, y tienen por coordenadas (x0,y0), (x1,y1), respectivamente; las coordenadas de las cuatro esquinas son:

Y lo dibujamos con las siguientes funciones:

   Linea(x0,y0,x1,y0);
   Linea(x0,y0,x0,y1);
   Linea(x1,y0,x1,y1);
   Linea(x0,y1,x1,y1);

Ahora veamos cómo dibujar un polígono. Para ello, lo único que realmente necesitamos conocer son las coordenadas de sus vértices, pues luego no tendremos más que trazar líneas entre estos puntos para pintarlo. Sin embargo, cualquier elección para trazar líneas entre vértices no nos van a dibujar un polígono. Por ejemplo, si tenemos los siguientes cinco vértices:

trazando líneas al azar podríamos conseguir la siguiente figura:

cuando en realidad, la figura sería una de las siguientes cuatro posibles:

Para evitar el caso que hemos visto trazando líneas al azar, tenemos que seguir un criterio a la hora de almacenar los vértices. La idea es la siguiente: elegimos el orden en que queremos pintar los vértices, y los guardamos en un vector, siguiendo ese orden. Una vez guardados, lo que hacemos es pintar una línea que una el primer vértice con el segundo, una línea que una el segundo con el tercero, …, y así seguimos, pintando una línea que una el penúltimo con el último, y una línea que una el último con el primero (si no fuera así, no cerraríamos la figura). Debe quedar claro que la ordenación de los vértices es algo que depende del programador.

Como ejemplo, vamos a tomar la primera de las cuatro figuras representadas antes; elegiremos una ordenación para los vértices y pintaremos la figura siguiendo esa ordenación:

Creamos una estructura en la que almacenar las coordenadas de un punto bidimensional, por ejemplo:

   REGISTRO Punto 2 D?
      x, y : real;
   FIN

y, a continuación, creamos un vector con cinco componentes, al que seguidamente le asignamos las coordenadas (unas cualesquiera puestas a modo de ejemplo):

  Punto 2 D p1[5];

  p1[0].x ← 100;
  p1[0].y ← 20;
  p1[1].x ← 60;
  p1[1].y ← 20;
  p1[2].x ← 50;
  p1[2].y ← 60;
  p1[3].x ← 80;
  p1[3].y ← 40;
  p1[4].x ← 110;
  p1[4].y ← 60;

Finalmente, podemos dibujar las líneas, aprovechando un bucle:

  DESDE i=0 MIENTRAS i<4 HACER
    Linea(p1[i].x,p1[i].y,p1[i+1].x,p1[i+1].y);
  FIN
  Linea(p1[4].x,p1[4].y,p1[0].x,p1[0].y);

Queda como ejercicio al lector ordenar los vértices en las tres posibilidades restantes y dibujar la correspondiente figura. Y con esto finaliza el capítulo dedicado al dibujo de polígonos. En el próximo ya pasamos al estudio del dibujo de circunferencias


Google