Gráficos 3D (2)
Pulsa aquí para bajarte los programas de demostración
Inicio aquí la segunda parte de mi artículo sobre gráficos vectoriales, publicado en el número anterior de la revista. Dado que me voy a basar mucho en todo lo dado en el artículo anterior, os recomiendo que, si no lo habéis hecho, os lo leáis; si no, muchas cosas os sonarán a FORTH :-).
Y como dicen que rectificar es de sabios, creo que la mejor forma de empezar es corrigiendo algunos detalles que se me escaparon en el artículo anterior.
Sobre el QuickBasic
En la primera parte aseguraba yo que las versiones más antiguas del QUICKBASIC (concretamente la 2.01) son más rápidas que las recientes (comparando con la 4.0). En realidad no es del todo cierto. Me explico:
Parece ser que, en algunos ordenadores, la versión 4.0 es tan rápida como la 2.01, mientras que en otros no. Concretamente, yo tengo un PS/2 8086 a 8Mhz, y la versión 4.0 va exactamente igual que el QBASIC, incluso al hacer un fichero EXE, o con el BRUN40. Sin embargo, en otros como un 486 a 50, esta versión, desde el editor, va unas cuatro veces más rápido que el QBASIC en ese mismo ordenador; y compilando en un fichero EXE, o con BRUN40, funciona del orden de siete veces más rápido que el QBASIC. ¿Por qué ocurre ésto? No lo sé, pero sospecho que el coprocesador matemático puede estar involucrado :-?
Uso del color especial en el algoritmo del pintor
Decía yo que, al usar el algoritmo del pintor, era necesario pintar cada cara con un color específico, y luego de nuevo, pero ya con el color que realmente le corresponde. En realidad, esto solo hay que hacerlo al usar la primera rutina de rellenado (la que necesita saber un punto del interior de la figura); usando la segunda rutina de rellenado, esto no es necesario.
El por qué de esto es que el primer algoritmo se basa en la imagen que hay en la pantalla para rellenarla. Por eso, si no se usa primero el color específico (denominado "clave mate") una cara anterior que estuviese pintada con el mismo color que la que estamos pintando ahora podría echar al traste la operación. Sin embargo, el segundo algoritmo se basa en ciertos datos de la cara, que almacena en una matriz. Como esos datos son sólo de la cara actual, no hay peligro de que los puntos de una cara anterior interfieran con los de la cara actual. Esa es la razón de que los programas que usan el segundo algoritmo se puedan usar en modos de pantalla monocromos, mientras que el primero no, pues necesita como mínimo tres colores: el de fondo, el color definitivo para las caras, y la "clave mate".
¿Qué es una pila?
Al explicar como trabaja el primer algoritmo de rellenado, repito muchas veces "meter las coordenadas en la pila". Pero ¿qué es una pila?.
Una pila es un tipo de "estructura de datos". Por decirlo de algún modo, es un sistema de almacenar información con una estructura determinada. Existen muchos tipos de estructuras de datos, aparte de las pilas. Están también las colas y las listas.
¿En qué consiste una pila? Veámoslo con un ejemplo: supongamos que llegamos a la cafeta de... Teleco, por ejemplo, y pedimos un bocata tortilla. En cosa de tres minutos lo tendremos servido en la barra, encima de un plato. En nuestro ejemplo no nos va a interesar el bocadillo, sino el plato, precisamente porque en esta cafeta (y en cualquiera) los almacenan formando una pila, unos encima de otros. ¿Qué pasa cuando se necesita un plato? Se coge el de arriba. ¿Y cuando se han lavado los usados? Se añaden encima de los que ya había. ¿Y si descubren que uno de en medio está roto? Quitarán los que había encima de él y lo tirarán.
Pues bien, de igual manera funcionan las pilas en los ordenadores, pero con números. Cuando se mete un número en la pila, se coloca encima de todos los demás, y cuando se saca uno, se coge el de arriba de todo. Vemos que no podemos coger un dato concreto, sino que siempre tendremos que coger el primero. Por eso, para coger un dato de en medio tenemos que quitar primero los datos que hay antes que él.
Si nos fijamos, vemos que el último dato que a entrado en la pila es el primero que saldrá cuando cojamos algo de ella. Por eso a las pilas se las denominan LIFO: Last In, First Out (Ultimo en Entrar, Primero en Salir).
La manera de trabajar con ellas es la siguiente: se define un área de memoria (en nuestro caso una matriz de una dimensión) y un puntero que apunte al primer elemento de dicha área (en nuestro caso, una variable a cero, que nos marca el primer elemento de la matriz). Cada vez que queramos almacenar un elemento en la pila, lo meteremos en la posición del área (matriz) que indique el puntero (nuestra variable), y lo incrementaremos en una unidad (el puntero/variable), de modo que pase a apuntar al siguiente elemento de la pila (matriz).
Si lo que queremos es sacar un dato de la pila, decrementaremos en una unidad el puntero (o la variable) y tomaremos el elemento de la matriz que nos indique. OJO AL ORDEN: primero decrementamos y luego tomamos el dato al que apunta. Esto es así porque, al meter datos, primero los almacenamos y luego incrementamos el puntero.
En los programas va incluida una protección ante el posible caso de que la pila quede vacía. No es una característica "estándar", pero resulta necesaria para saber cuando la figura está totalmente rellena.
Existen otras estructuras de datos, pero no las voy a comentar, pues con esto se podría hacer un artículo completo. ¿Alguien se anima?.
El algoritmo de ordenación
En el apartado dedicado al algoritmo del pintor expliqué que era necesario ordenar las caras desde la más lejana a la más cercana, para así pintarlas en dicho orden; sin embargo, no expliqué ningún algoritmo de ordenación, ni siquiera el usado en los programas. Vamos a verlo con detalle:
El método que he empleado es el denominado "de selección". En él, cojo el primer elemento a ordenar y lo comparo con los elementos que le siguen; si es mayor, lo dejo como está; si es menor, lo intercambio con dicho elemento, y sigo comparando hasta llegar al final. De este modo ya tengo que el primer elemento es el mayor de todos.
Ahora cojo el segundo, y lo comparo con los que le siguen (no debo hacerlo con el primero porque ese ya estoy seguro de que está en su sitio), y repito el proceso; así para cada valor de la lista, excepto el último, que no hace falta porque no tengo con quien compararlo.
Este método es bastante simple e intuitivo, pero no es el mejor. Tiene la ventaja de que el tiempo de ordenamiento depende únicamente del número de elementos, y no de si estos están parcialmente ordenados o no. Por eso puede ser conveniente usarlo en animaciones en tiempo real, para que todos los fotogramas tarden lo mismo en calcularse, y no unos más que otros. Sin embargo, en otros casos la rapidez es esencial, sin importar el que el tiempo entre fotogramas sea variable. En dichos casos es conveniente usar otros algoritmos más perfeccionados, como QUICKSORT o MERGESORT.
El explicar estos algoritmos requeriría de un artículo entero (¿Alguien se anima...?). Podéis encontrar información sobre esto en, por ejemplo, los números 28 a 31 de la revista MICROHOBBY, en un artículo firmado por Javier Alemán, o bien en los números 5 y 6 de la revista DATA BUS, en un artículo de Jesús Cea Avión.
Corrección al cálculo del nivel de luz ambiental
Cuando expliqué la iluminación por sombreado dije que, normalmente, los objetos están bañados no sólo por la luz del foco, sino también por una luz ambiental, y que la mejor manera de realizarla era sumársela a la cantidad de luz que recibía cada cara. Pues bien; hablando de esto con Pablo Costas, descubrió que el efecto resulta más realista si, en vez de sumar a la luz puntual el nivel de luz ambiental, la comparamos con ésta; esto es: si el nivel de luz que recibe una cara es mayor que la luz ambiental, se pinta tal cual, pero si es menor, se pinta con el nivel de luz ambiental. De esta forma los objetos resultan mucho más realistas.
La variable de relación de aspecto
Al explicar el problema de la relación de aspecto, comenté que había una variable en los programas, RELAS, que contenía dicho valor. En principio, este valor no se debe cambiar, ya que achataría las figuras que salen en pantalla. Sin embargo, no dije que los usuarios de ciertos portátiles, así como de otros modelos de ordenador con relaciones de aspecto distintas, sí deben cambiarla, para ajustarla al valor concreto en cada caso. Para hacerlo, deben medir el ancho de la superficie útil de la pantalla, y dividirlo por el alto de dicha superficie útil (no importa la unidad de medida). El valor obtenido es el que deben usar en la variable RELAS. De esta manera las figuras saldrán perfectamente proporcionadas, si bien es posible que algo de pantalla quede desaprovechada.
Por poner un ejemplo, los TOSHIBA T1200 tienen una pantalla rectangular, con una relación de aspecto de 1.64, frente al 1.3 (4/3) de las pantallas normales, lo que hace que las figuras salgan comprimidas verticalmente. Ajustando la variable, saldrán perfectas.
Uso de modos de pantalla extra
Los programas que incluía el artículo anterior (y éste) solo admiten los modos de una tarjeta VGA o inferior. Eso significa que, en caso de disponer de una tarjeta superior, del tipo SUPER VGA, no podremos usar sus modos extra. Esto es porque el QBASIC no los admite. Sin embargo, tengo entendido de que algunos otros tipos de BASIC más avanzados sí los admiten. Por eso sería interesante poder aprovecharlos.
Si el editor/compilador de que disponemos permite trabajar en esos modos extra, para usarlos solo debemos ir al final del programa, en la subrutina PREPARAR. Allí existe una nueva subrutina por cada tarjeta. Para usar una no contemplada, basta con colocar en la variable XMAX el ancho en pixels de la pantalla en dicho modo; en YMAX el alto, y en NUMCOLOR el número de colores simultáneo que admite, que puede ser 2, 4, 16 o 256 (si el modo soporta 32768, 65536 o 16 millones de colores, se debe poner 256). Por último, debéis activar dicho modo con el SCREEN correspondiente.
Aclarado todo esto, empecemos a ver cosas nuevas.
Uso de colores en el sombreado de objetos
Actualmente es rara la tarjeta gráfica que no dispone de 256 colores simultáneos, si no más. Aprovechando esta capacidad, podemos realizar el sombreado de objetos con distintos tonos de un color, en vez de usar tramas, consiguiendo un efecto mucho más realista. Pero primero tenemos que ver un poco como podemos definir los colores que nos interesan. Para ello vamos a ver como trabaja una tarjeta típica, así como diversos conceptos útiles.
1: Cómo se genera el color
Todo el mundo sabe que cualquier color se puede descomponer en una mezcla de tres: rojo, azul y amarillo, en el caso de usar pinturas; o rojo, azul y verde, si se trabaja con luz. Este último caso es el que nos atañe, así que vamos a estudiarlo con más detenimiento; pero antes vamos a ver como funciona un monitor:
Una pantalla monocroma de ordenador tiene en su parte posterior un dispositivo llamado "cañón". Este dispositivo, basándose en el efecto termoiónico, es capaz de emitir electrones. Este rayo de electrones puede modularse, de modo que tenga más o menos potencia, regulando el voltaje que excita el cañón. Además, podemos desplazar el haz en cualquier sentido -arriba, abajo, derecha, izquierda- por medio de unos electroimanes situados cerca del cañón, cuyo conjunto recibe el nombre de "yugo" (por eso no se deben poner disquetes encima o muy cerca del monitor, pues los campos magnéticos que se producen pueden alterar la información).
La parte frontal de la pantalla está recubierta de un compuesto de fósforo, el cual, cuando es alcanzado por el rayo de electrones, emite luz. La cantidad de luz que emita, el brillo, dependerá de la intensidad del rayo: a mayor intensidad, más brillante aparecerá el punto. Por otro lado, el color que emita dependerá del tipo de compuesto de fósforo usado.
En un monitor a color, existen tres cañones, los cuales envían su propio haz de electrones. Dado que los tres haces pasan por el mismo yugo, los tres se moverán perfectamente al unísono, apuntando casi al mismo sitio. Sin embargo, la intensidad de cada uno se puede regular independientemente de los demás.
Surge otra diferencia en la pantalla. En este caso, en vez de estar toda recubierta por un único tipo de fósforo, presenta multitud de puntos de un fósforo rojo, otro verde, y otro azul, los tres colores básicos, agrupados de tres en tres. Además, justo detrás del fósforo hay una fina lámina metálica, perforada de tal manera que cada uno de los cañones solo pueda acertar a un tipo de fósforo: o al rojo, o al verde, o al azul.
De este modo, a medida que los rayos van recorriendo la superficie de fósforo, van excitando el punto que le corresponde, haciendo que emitan un nivel determinado de luz. Luego el cerebro junta esos tres colores básicos, cada uno con su intensidad correspondiente, y ve el color que realmente le corresponde.
Ahora que sabemos esto, veamos el funcionamiento de la circuitería de video. Esta, lo que hace, es leer el color con el que queremos pintar el pixel sobre el que está en estos momentos el rayo, y lo traduce a tres valores: uno para el rojo, otro para el verde, y otro para el azul.
Estos tres valores pasan a través de un conversor digital/analógico, de modo que se obtienen tres tensiones, una para cada uno de los tres colores. Estas tensiones son enviadas al tubo, y éste las usa para controlar la intensidad de cada uno de los tres cañones de electrones. De este modo, el fósforo emite una luminosidad proporcional al valor que le entrega la tarjeta de video.
Llega ahora el momento de determinar qué valores nos darán el color deseado. Para esto, solo debemos recordar un poco algunas propiedades de la mezcla de colores; veamos.
Supongamos que a cada uno de los haces le podemos entregar un valor entre 0 y 255 (8 bits). Esto significa que, si enviamos un 255 al rojo, y un cero al verde y al azul, obtendremos un punto de color rojo puro. Lo mismo sucede con los otros dos colores básicos.
Hasta aquí, nada nuevo. Pero, ¿qué pasa si activamos dos colores a la vez? Bien; supongamos que activamos el rojo y el verde, ambos con un nivel 255. Obtendremos un bonito color amarillo. Si mezclamos verde y azul, saldrá cyan (celeste). Las posibles combinaciones son las siguientes:
Azul | Rojo | Verde | Color |
0 | 0 | 0 | Negro |
255 | 0 | 0 | Azul |
0 | 255 | 0 | Rojo |
255 | 255 | 0 | Rosa |
0 | 0 | 255 | Verde |
255 | 0 | 255 | Celeste |
0 | 255 | 255 | Amarillo |
255 | 255 | 255 | Blanco |
Aquí estoy usando el valor 255, que es el máximo, pero ¿y si uso otro valor, como 128? Pues que el resultado será el mismo color, pero más oscuro. Esto nos permite hacer gradaciones de color.
Sin embargo, de este modo hay muchos colores que no aparecen ¿dónde está el naranja; y el marrón...?
Estos colores surgen de la mezcla de dos colores de la tabla anterior, en distintas proporciones. Veamos como hacerlo:
El naranja se obtiene de la mezcla de rojo y amarillo. Esto significa que debemos mezclar sus códigos. La manera de hacerlo es la media aritmética:
0 / 2 | 255 / 2 | 0/2 | Rojo | |
+ | 0/2 | 255/2 | 255/2 | +Amarillo |
- | ------ | ------ | ------ | ------ |
0 | 255 | 128 | Naranja |
Vemos que el color 0,255,128 es el naranja. La razón de hacer la media aritmética es que el resultado nos aparezca en el rango 0-255. De aquí podemos deducir una importante conclusión: si dividimos las tres componentes de un color por un mismo número, el color no varía, pero sí su brillo. De este modo, si tomamos las componentes de un color y las dividimos entre dos, obtendremos el mismo color pero con la mitad de luminosidad.
Veamos ahora el concepto de paleta. Si quisiésemos que cada uno de los puntos pudiese tener su propio color, con sus tres componentes respectivas, sería necesario disponer de bastante memoria, de forma que podamos almacenar los tres bytes necesarios para cada píxel. Para arreglarlo de alguna forma, se ideó el sistema de paleta de colores.
Digamos que queremos poder disponer de 256 niveles para cada color básico. Eso nos da la friolera cantidad de 16.777.216 colores simultáneos. Para muchas aplicaciones, esto es excesivo, pues con menos colores, 256 normalmente, es más que suficiente. ¿Como lo resolvemos? Muy fácil: en la memoria de video almacenamos el número de color que deseamos, de 0 a 255, y luego, en otra zona de memoria aparte, mantenemos una tabla que contiene las tres componentes para cada uno de los 256 colores disponibles. De este modo, indicamos que el color 0 es negro, el 1 rosa, el 2 dorado, el 3 bermellón,... Así, la cantidad de memoria necesaria es mucho menor, y disponemos de la misma capacidad de color.
El uso de este sistema significa que, si bien el ordenador puede reproducir 16.777.216 colores, solo 256 de estos pueden aparecer de manera simultanea. Tenemos, pues, una paleta de 16.777.216 colores, o 24 bits, y de ella escogemos los 256 que nos interesen para la aplicación concreta que estemos desarrollando.
Por tanto, antes de poder pintar algo en la pantalla, debemos definir la paleta que vamos a usar, para lo cual, los lenguajes traen sus instrucciones correspondientes.
Tal vez alguien esté pensando que estoy liando un poco los términos con la palabra paleta, pues hay gente que usa esta palabra para definir el número total de colores que puede representar el ordenador (los 16.777.216), y gente que la usa para indicar el subconjunto de colores que pueden aparecer simultaneamente (los 256). Yo voy a usar la misma palabra para ambas cosas, pero de manera que quede claro a cual me estoy refiriendo.
2: Análisis de un caso: el QBASIC
Vamos a ver como definiríamos una paleta en el QBASIC de MICROSOFT. Para ello, debemos conocer antes las características concretas de nuestro equipo, en este caso un PC con tarjeta MCGA, VGA o SVGA.
Lo primero es que en el modo de 320x200 (el único de 256 colores que admite el QBASIC), la paleta no es de 24 bits, sino de 18. Esto significa que disponemos de 64 niveles para cada uno de los tres colores primarios (chapucilla :-). Por tanto, no podemos disponer de 256 tonos distintos de un mismo color, sino solo 64. Esto solo ocurre en este modo por culpa de las antiguas VGAs. Las modernas SVGAs admiten los 16 millones de colores, pero solo en los modos extendidos para mantener la compatibilidad.
Después de esta primera sorpresa, llega la segunda: cuando usamos la instrucción PALLETE, que nos permite asignar a cada color sus tres componentes, los valores mayores de 63 no son aceptados.
La razón es que necesita que los 6 bits de cada color se le entreguen en bytes separados. Eso significa que, para hallar el valor que contiene las tres componentes, debemos usar la ecuación:
Siendo ROJO, VERDE y AZUL los valores de las componentes, en un rango de 0 a 63.
La razón de hacerlo así se debe únicamente a razones de simplicidad en el diseño del circuito físico de la tarjeta. De entregar el valor todo junto, habría sido necesario complicar la circuitería.
Una vez entendido esto, es fácil crear uno mismo sus propias paletas. Por ejemplo, si queremos tener una con 64 tonos de rojo, verde, azul y blanco, solo tenemos que asignar a los colores 0 a 63 los tonos 0 a 63 en la variable ROJO; a los colores 64 a 127 los mismos tonos en VERDE; a los colores 128 a 191, los mismos tonos en AZUL; y por último, a los colores 192 a 255, esos tonos en las tres variables.
El programa DEMO1.BAS genera esta paleta, y la muestra en pantalla. Para poder usarlo, es necesario disponer de una tarjeta gráfica MCGA, VGA o SVGA.
Por su parte, el programa DEMO2.BAS realiza la rotación de una copa en 3D con sombreado de superficies usando una paleta de 64 tonos, en vez de tramas. Las necesidades son las mismas que para el programa anterior.
Este programa incluye una pequeña opción al principio: pregunta si se quiere sombreado en tiempo real. Si se dispone de un ordenador rápido, se puede contestar que sí a la pregunta, tras lo cual el ordenador nos preguntará la colocación de la fuente de luz. Una vez contestado, pintará la copa ya sombreada, y la podremos mover en este estado. Con un ordenador lento no se debe escoger esta opción, pues entre movimiento y movimiento el ordenador tiene que recalcular y sombrear toda la copa. En este caso, se debe contestar "No", con lo que el sombreado se realizará al apretar la tecla R, y el movimiento será sobre la figura en representación alámbrica.
Estos dos programas se salen un poco de la tónica general de que cualquier usuario los pueda usar, con independencia de la tarjeta gráfica de que disponga. Sin embargo, he considerado necesario incluirlos, pues actualmente ningún ordenador viene con una tarjeta que no los acepte, y creo que es necesario saber usar este sistema.
Generación de entornos virtuales
Vamos a ver ahora como podemos generar un entorno virtual, por el que nos podamos mover con total libertad.
Lo primero que necesitamos es definir una estancia. Podemos hacerlo de muchas maneras, por ejemplo, creando un objeto que sea, precisamente, la estancia por la que nos queremos mover. Otro sistema, más lógico, es definir cada objeto de la estancia por separado y pintarlos todos en unas coordenadas relativas a un punto determinado, de modo que si muevo una, se muevan todos en el mismo sentido.
Ahora veamos las diferencias que hay con respecto a dibujar un simple objeto en pantalla:
Para empezar, el origen de coordenadas debe estar situado en nuestro ojo, y no en la pantalla, para que todas las rotaciones se efectuen tomándonos a nosotros como centro. Recordemos lo que decía en el artículo anterior:
El segundo problema es el que más quebraderos me dió, pues las ecuaciones anteriores suponen que el origen de coordenadas está en el ojo del observador, mientras que yo suponía (triste ironía) que el origen estaba en la pantalla. Así, cuando intentaba colocar un punto en la coordenada (X,Y,0), siempre se me paraba con un error de división por cero. Por tanto, para desplazar el origen hasta la pantalla y evitarnos errores, usaremos las ecuaciones
Por tanto, para proyectar los puntos debemos usar las ecuaciones:
Por tanto, a partir de ahora, si rotamos el/los objetos que forman la estancia, rotarán alrededor de nuestro ojo, y no alrededor de su eje.
Antes de ver el recorte de líneas, vamos a ver que entendemos por "piramide de visión".
Al trabajar con estos gráficos tridimensionales, debemos suponernos que estamos viendo el mundo a través de una ventana, que en nuestro caso es el monitor. Si unimos nuestro ojo con cada uno de los vértices de él, se nos forma una pirámide de cuatro lados, que nos limita los objetos que podemos ver. Lo veremos mejor si suponemos que estamos dentro de una caja que solo tiene una pequeña ventana por la que podemos ver al exterior. Todo lo que esté fuera de esa pirámide formada por nuestro ojo y los cuatro vértices quedará también fuera de nuestro ángulo de visión (ojo, las rectas que unen nuestro ojo con cada uno de los vértices, no terminan en éstos, sino que siguen hasta el infinito).
¿Qué pasaría si proyectamos un punto que cae fuera de nuestra pirámide de visión? Pues que las coordenadas proyectadas se saldrán fuera de la pantalla, o sea, fuera del rango 0-dx, o 0-dy. Por tanto, debemos poder recortar esas líneas para hallar su intersección con las caras de la pirámide, y evitar así que el ordenador nos de un error por intentar pintar fuera de la pantalla.
Tal vez alguno dirá que el QBASIC no tiene ningún problema en que las líneas se salgan fuera de la pantalla; sin embargo, esto no es del todo correcto: con valores muy altos, sí da error. Además, no todo el mundo trabaja con QBASIC, por lo que es necesario conocer los algoritmos de recorte de líneas, para el caso, por ejemplo, de trabajar en ENSAMBLADOR.
Sin embargo, antes de ver esto debemos ver otra peculiaridad: ¿qué pasa con los puntos situados DETRAS del observador? Estos no se deberían ver en teoría; sin embargo, si les aplicamos la formula, y si se encuentran dentro de la parte de atras de la pirámide de visión (ficticia, desde luego, porque por la parte de atras del observador NO existe tal pirámide de visión), nos saldrán unas coordenadas de pantalla perfectamente válidas, y que serán las simétricas con respecto al centro de la pantalla de las que obtendríamos si mirásemos hacia el punto (es decir, si diésemos media vuelta).
Por todo esto, vemos que tenemos que hacer dos recortes de líneas: por un lado, debemos recortar todas aquellas que pasen de la parte delantera a la parte trasera del observador (las que estén en su totalidad detrás del observador simplemente no debemos pintarlas), y por otra parte, todas aquellas que se salgan de los límites de la pantalla.
Recorte de las líneas que pasan por detrás del observador
Las líneas que tengan uno de sus vértices delante del observador y el otro detras deben ser recortadas para que no aparezcan figuras extrañas en la pantalla. Para ello, simplemente debemos hallar su intersección con el plano XY, que es el que separa la parte anterior y posterior del observador. Esta intersección es muy fácil de hallar. Para ello, vamos a usar la ecuación Punto-Pendiente de la recta:
Siendo (X0,Y0) un punto perteneciente a la recta, y m la pendiente.
Veamos todos los pasos:
Supongamos que tenemos XI, YI y ZI como coordenadas iniciales de la recta, y XF, YF y ZF como coordenadas finales.
Lo primero que debemos comprobar es si ZI y ZF son ambas negativas, en cuyo caso la recta está totalmente en la parte trasera del observador, y no hara falta pintarla.
Si alguna de las dos coordenadas es positiva, procederemos al recorte y luego a pintarla en pantalla. Veamoslo:
Comprobamos primero si ZI es igual a ZF, en cuyo caso la recta será paralela al plano XY. La razón de hacer ésto es que, para hallar la pendiente de la recta, debo dividir por la resta de esas dos variables, y si son iguales, ésta será cero, dando error.
A continuación, si la comparación da que son distintas, calculo las dos pendientes de la recta:
pendiente en el plano YZ: M1 = (XF - XY) / (ZF - ZI)
pendiente en el plano XZ: M2 = (YF - YI) / (ZF - ZI)
Y ahora, simplemente calculamos el punto de corte de la recta con el plano XY, si es que existe. Para ello hacemos lo siguiente:
Si ZI <= 0 significa que este punto está detras, luego no nos vale. Debemos hacer:
- ZI = 1
- XI = XF + (M1 * (ZI - ZF))
- YI = YF + (M2 * (ZI - ZF))
De este modo, obtenemos el punto de corte de la recta con el plano XY en la coordenada ZI = 1. La razón de no coger ZI = 0 es que nos daría DIVISION POR CERO al realizar la proyección; así que cogemos una unidad más hacia adelante.
Si ZF <= 0 significa que este punto está detras, luego no nos vale. Debemos hacer:
- ZF = 1
- XF = XI + (M1 * (ZF - ZI))
- YF = YI + (M2 * (ZF - ZI))
De este modo, obtenemos el punto de corte de la recta con el plano XY en la coordenada ZF = 1.
Este proceso se puede simplificar un poco eliminando una de las comparaciones, pero lo dejo como ejercicio. ¡Hala, a romperse un poco las meninges! :-)
Con esto, ya hemos eliminado los trozos de las rectas que caían por detrás del observador; sin embargo, aún nos queda por recortar las que, al proyectarlas en dos dimensiones, se salen fuera de la pantalla. Estas rectas son aquellas que se salen de la pirámide de visión por los lados y/o por arriba y abajo. El proceso a seguir es el siguiente:
Lo primero que debemos comprobar es si la recta está totalmente fuera de la pantalla o no. Para ello llamaremos a sus coordenadas como (XI,YI)-(XF,YF). Ahora, la recta estará totalmente fuera de la pantalla si se cumple cualquiera de estas condiciones:
- XI < 0 y XF < 0
- XI > XMAX y XF > XMAX
- YI < 0 y YF < 0
- YI > YMAX y YF > YMAX
Lo cual es lógico: si las dos coordenadas X son menores que cero, los dos extremos estarán fuera de la pantalla por la izquierda; y si ambos son mayores que XMAX (la coordenada máxima en X de la pantalla), la línea estará fuera por la derecha. Lo mismo pasa con las Y: si son menores que cero, la línea estará por encima del monitor, y si son mayores que YMAX, la línea estará por debajo del monitor.
En cualquiera de esos casos no debemos pintar la línea.
Pero si no se cumple ninguno, entonces debemos hacer lo siguiente:
- Calculamos DIF1 = XF - XI
- Calculamos DIF2 = YF - YI
- Si DIF1 <> 0 y DIF2 <> 0 (no es una recta ni
horizontal ni vertical, sino inclinada)
- Calculamos la pendiente y su inversa:
- MX = DIF2 / DIF1
- MY = DIF1 / DIF2
La razón de que DIF1 y DIF2 tengan que ser distintas de cero es que hacemos una división con ambos en el divisor.
Llamamos:
- X = XI
- Y = YI Para tener un punto de la recta.
Ahora debemos comprobar si cualquiera de las coordenadas se sale fuera de la pantalla, y en ese caso, proceder al recorte por medio de la ecuación de la recta:
- Si XI < 0
- XI = 0
- YI = Y - MX * X Calculamos el valor de YI para XI = 0
- Si YI < 0
- YI = 0
- XI = X - MY * Y Calculamos el valor de XI para YI = 0. Usamos la inversa de la pendiente.
- Si XI > XMAX
- XI = XMAX
- YI = Y + MX * (XI - X) Calculamos el valor de YI para XI = XMAX
- Si YI > YMAX
- YI = YMAC
- XI = X + MY * (YI - Y) Calculamos el valor de XI para YI = 0. Usamos la inversa de la pendiente.
Lo mismo lo debemos hacer con las coordenadas XF e YF, para recortarlas en caso de que se salgan de la pantalla.
Una vez hechos estos cálculos, si alguna coordenada X (ya sea XI o XF) se nos queda fuera de los límites, significa que esa línea está totalmente fuera de la pantalla, por lo que no debemos imprimirla.
Hasta aquí la parte para la que DIF1 <> 0 y DIF2 <> 0. Ahora:
- Si DIF1 = 0 Esto significa que la recta es vertical
- Si YI < 0 entonces YI = 0
- Si YI > YMAX entonces YI = YMAX
Lo mismo para YF. La razón de no usar la ecuación de la recta es que las X son constantes, luego para cualquier valor de Y van a valer lo mismo.
- Si DIF2 = 0 Esto significa que la recta es horizontal
- Si XI < 0 entonces XI = 0
- Si XI > XMAX entonces XI = XMAX
Y lo mismo para XF.
Tras todos estos pasos, podemos estar seguros de que las coordenadas de la recta estarán dentro de los márgenes
- 0 <= XI , XF <= XMAX
- 0 <= YI , YF <= YMAX
por lo que solo nos quedará pintarla en pantalla, usando la instrucción o algoritmo correspondiente.
Visto esto, ya podemos aplicarlo a cualquier objeto. Pero aún no sabemos como avanzar por él. Para ello, basta con conocer el principio de la relatividad de movimiento. Es lo mismo avanzar diez metros, que el terreno retroceda otros diez. Por tanto, cuando queramos avanzar X pasos, debemos desplazar la habitación -X pasos. Si queremos girar A grados, debemos rotar -A grados la habitación. Así de fácil. Si nuestra habitación está compuesta por varios objetos separados, debemos desplazar y/o rotar cada uno lo mismo.
El programa DEMO3.BAS es una demostración de todo esto. En él nos podremos mover por una habitación virtual. El programa no es ninguna maravilla en lo que a aspecto gráfico se refiere, pues no incluye ninguna rutina de eliminación de caras ocultas ni de sombreado. Esto lo he hecho así para conseguir buena velocidad en ordenadores lentos (con un 486 sí se podrían haber añadido esas rutinas).
Existe una variable especial en el programa: NIVEL. Esta variable especifica el niver de detalle a usar. Si vale 0, solo se dibujará la puerta, la mesa y las paredes; si vale 3, se pintarán todos los objetos. Si vale 1 u 2 se pintarán algunos objetos más, pero no todos. Lo he hecho así porque en ordenadores lentos puede tardar mucho en dibujar todos los objetos. Si tienes un ordenador rápido, ponla a 3.
Algoritmos de sombreado suavizado
Los objetos sombreados con las rutinas anteriores no tenían muy buena calidad, debido a que no eran figuras de revolución perfectas, sino aproximaciones hechas con superficies planas. Lo ideal sería tratar cada punto de la imagen con la ecuación de luz, de modo que reciba la cantidad correcta. Sin embargo, en la práctica necesitamos otro sistema, pues este consumiría demasiado tiempo. Para ello, se ideó inicialmente el algoritmo de sombreado suavizado de Gouraud y posteriormente el de Phong.
El algoritmo de Gouraud
El algoritmo de Gouraud se basa en calcular los vector normales en un vértice común a varias caras y realizar el promedio entre ellos, interpolando luego los colores por la cara. Veamos que quiero decir con esto:
Supongamos que tenemos una cara de tres lados, A, B y C. Esos vértices pertenecen no solo a esa cara, sino también a las caras que estén en contacto con la primera. Al usar los algoritmos de iluminación hasta ahora, veíamos cada uno de esos vértices como tres (o más, depende del número de caras en contacto) vértices distintos, cada uno con su nivel de luz correspondiente. Esto es correcto en objetos con caras, pero no es un efecto deseable en una figura de revolución, pues se supone que es un solo punto. Entonces, si suponemos que el punto A pertenece a las caras a, b y c, lo que haremos será hallar los tres vectores normales en A respecto de a, b y c. Esos tres vectores los promediamos (con la media aritmetica), y nos dará el vector normal que habría en ese punto si la figura fuese realmente de revolución, y no una aproximación hecha con superficies planas.
Una vez que hemos calculado los vectores normales teóricos en cada punto, hallamos la iluminación de estos. Esto nos da que en la cara que teníamos, el punto A tiene un nivel de luz, el B otro y el C otro más, los cuales no tienen por qué ser iguales. ¿Cómo podemos pintar la cara, si tenemos tres niveles distintos? La solución consiste en interpolar los colores: si en A tiene un valor y en B tiene otro, en el punto medio de ambos tendrá la media.
Se puede hacer una simplificación, pues es fácil demostrar que el nivel de iluminación del punto calculado con la media de los tres vectores normales es igual a la media de los niveles de iluminación calculados con cada uno de los vectores normales. De esta forma, calcularemos la iluminación del punto con respecto a cada cara y hallaremos su media.
Para ahorrar tiempo de ejecución, podemos calcular primero la iluminación de cada punto (la media) y almacenarla en una tabla. Así no hará falta calcularlo repetidas veces, una por cada cara que comparta el vértice.
¿Como interpolamos los colores? Simple: supongamos que tenemos una cara triangular, formada por los puntos A=(X0,Y0), B=(X1,Y1) y C=(X2,Y2). El nivel de luz en cada punto será La, Lb y Lc respectivamente. Sabemos que la ecuación de la recta que usamos es:
siendo 'a' un valor entre 0 y 1. Pues bien, esta ecuación lo que hace es promediar los puntos situados entre X1 y X0 (o Y1 e Y0 en la otra), por lo que podemos usarla para hacer el promedio de los colores. El problema es que esto solo nos traza una línea. ¿Cómo podemos hacer para rellenar la figura completa?
La solución es simple: consideramos la línea que une los puntos A y B. Sería:
En el punto (X,Y) (un punto cualquiera de la recta AB), el nivel de luz será L(x,y) = La + a*(Lb-La) (un detalle importante: el valor de 'a' es el mismo PARA TODAS LAS ECUACIONES). Ahora sólo tenemos que repetir el proceso para cada punto de la recta que une C y (X,Y). De este modo, lo que hacemos es calcular un punto de la recta AB (el punto (X,Y) y promediar entre él y C; calcular el siguiente punto de la recta AB y promediar entre él y C. Y así hasta acabar con la recta AB.
Un pequeño organigrama sería:
- a=0 ;inicializo a
- mientras a<=1
- X = X0 + a*(X1 - X0) ;calculo X,
- Y = Y0 + a*(Y1 - Y0) ;Y
- L = La + a*(Lb - La) ;y el nivel de luz en ese punto.
- b=0 ;inicializo b
- mientras b<=1
- X' = X + b*(X2 - X) ;calculo X', promedio entre X y X2.
- Y' = Y + b*(Y2 - Y) ;lo mismo con Y'
- L' = L + b*(Lc - L) ;y con L
- PUNTO (X',Y',L') ;pinto un punto en X' Y' con color L'
- b = b + incremento ;incremento b con un valor decimal, cuanto más pequeño, mejor
- repite ;cierro el bucle b
- a = a + incremento ;incremento a
- repite ;cierro el bucle a
¿Qué hacer cuando tenemos caras de más de tres vértices? El proceso es el mismo, pero con una pequeña diferencia. Sean A1, A2, A3,..., An los n vértices de la cara. Lo que haremos será rellenar el triángulo formado por A1, A2 y A3; luego el triangulo formado por A1, A3 y A4; luego el formado por A1, A4 y A5, y así sucesivamente, hasta llegar al triangulo formado por A1, An-1 y An.
El programa DEMO4.BAS es un ejemplo de esto. Sombrea una copa usando el algoritmo de Gouraud. El resultado es muy realista sin duda. El programa puede funcionar con cualquier tarjeta, aunque a los usuarios de MCGA, VGA o SVGA les recomiendo el modo de 320x200 en 256 colores.
Al igual que en el programa DEMO2.BAS, al principio pregunta si se quiere sombreado en tiempo real. Si contestamos "SI", el programa sombreará la copa cada vez que la movamos. Debido a que el algoritmo de Gouraud consume mucho más tiempo que el de sombreado clásico, esta opción solo la recomiendo a usuarios de ordenadores 486DX2 o 486DX4 (o los equivalentes de otras marcas como Macintosh, Amiga o Atari: 68030 o 68040).
El algoritmo de Phong
El algoritmo de sombreado suavizado de Pong es una mejora del de Gouraud. Al igual que en el anterior, se calcula la media aritmética de los vectores normales en los vértices comunes, pero luego, en vez de promediar entre dos niveles de luz, lo que se hace es promediar entre dos vectores normales. Esto es: si Na es el vector normal en el vértice A (el calculado a partir de las medias de los vectores normales de cada cara), y Nb es el vector normal en el vértice B, para hallar la iluminación en cada punto de la recta AB lo que haremos será promediar el vector normal en cada punto y calcular con él la iluminación en dicho punto. Este es un sistema mucho más perfecto, pues aquí tenemos en cuenta más datos que en el anterior, concretamente orientación de los vectores. Esto permite conseguir imágenes muy realistas. Tiene, además, otra ventaja, y es que este sistema se puede usar con los algoritmos de raytracing, lo que permite usar figuras poligonales complejas que, de otro modo (partiendo de esferas, elipses, y demás figuras de revolución genéricas) serían muy difíciles de representar.
Esta obra está bajo una licencia de Creative Commons Reconocimiento-CompartirIgual 4.0 Internacional.