Sólo Programadores Banner 468x60
Este mes en Sólo Programadores
Contenido del CD-ROM
Índice temático
Suscripción

Representación gráfica de funciones tridimensionales.

En este artículo aprenderemos todo lo necesario para crear aplicaciones capaces de procesar y representar gráficamente funciones tridimensionales. Podremos crear un programa mediante el cual seamos capaces de introducir funciones para luego lograr visualizarlas utilizando el teclado.

EVALUACIÓN DE FUNCIONES MATEMÁTICAS

En realidad, el objetivo no consiste en crear un programa que sólo sea capaz de representar gráficamente una función determinada, o una serie limitada de funciones. Se trata de crear un programa mediante el cual el usuario pueda introducir la función que desee visualizar utilizando el teclado.

Por lo tanto antes de poder representar gráficamente una función, sea del tipo que sea, nuestro programa deberá ser capaz de procesarla de forma que pueda trabajar con ella. Es decir, deberá convertir la secuencia de caracteres introducidos por el usuario en una estructura de datos que sea capaz de manejar con eficiencia.
En los siguientes apartados estudiaremos la serie de transformaciones que debemos realizar para transformar la cadena de caracteres, que representa la expresión que se desea representar, en una estructura de datos que permita calcular valores de la función en cuestión.

Orden Operadores

1º - Potencia y raíz cuadrada

2º - Multiplicación y división

3º - Suma y resta

Figura 1a. Orden de prioridad entre operadores binarios.

(a+b)*c^d ? ((a+b)*(c^d))

Figura 1b. Añadir los paréntesis obviables.

LA PRIORIDAD ENTRE OPERADORES

Cuando se escribe una expresión matemática usamos paréntesis para indicar el orden en que deberán realizarse las operaciones. Para no trabajar con expresiones con demasiados paréntesis, lo que podría dificultar su legibilidad, se estableció el orden de prioridad entre los diferentes operadores matemáticos que todos conocemos. (Figura 1a) De esta forma, solamente es necesario el uso de los paréntesis cuando el orden en que deben realizarse las operaciones no es el indicado por las reglas de prioridad. Por lo tanto, la primera de las transformaciones que deberemos realizar a la cadena de caracteres de entrada consiste en colocar todos los paréntesis que normalmente obviamos. (Figura 1b)

LA NOTACIÓN POSTFIJA.

Una vez que se dispone de la expresión con todos los paréntesis correspondientes, el siguiente paso consistirá en pasar la expresión (que estaba en notación infija) a notación postfija. Antes de nada recordaremos cuáles son los tres tipos de notación para expresiones matemáticas (atendiendo a la posición en que se sitúan los operadores respecto de sus operandos): (Figura 2a)

- Prefija: En esta notación el operador precede a los operandos. El orden de operaciones queda claramente especificado por la posición de los elementos de la expresión (operadores y operandos). Se puede expresar como una lista de elementos o separando los operandos de cada operación con comas y entre paréntesis.

Notación- Ejemplo
Prefija: (lista)
* + a b ^ c d
Prefija:
*(+(a,b),^(c,d))
Infija:
(a+b)*c^d
Postfija:
((c,d)^,(a,b)+)*

Figura 2a. Diferentes notaciones matemáticas.

((a+b)*(c^d)) ? ((c,d)^,(a,b)+)*

Figura 2b. Pasar a notación postfija.

- Infija: Se trata de la notación que utilizamos habitualmente. El operador se sitúa entre sus operandos (en el caso de los operadores binarios). En el caso de operadores unarios (como el coseno) cómo solo hay un operador se usa la misma forma que en la notación prefija.

- Postfija: Exactamente igual que en el caso de la notación prefija, aunque en este caso el operador se sitúa detrás de los operandos. De hecho, para pasar una expresión de prefija a postfija lo único que tenemos que hacer es invertir la lista de elementos que componen la expresión.

En nuestro caso elegimos la forma postfija con paréntesis y comas. El motivo de esta elección reside en que la información se debe almacenar en una cadena de caracteres y que de esta forma resultará más fácil indicar la máxima información sobre posibles errores sintácticos que pueda contener la cadena de caracteres que queremos analizar. Además, restringiremos los operadores a unarios y binarios (ya que prácticamente todos los operadores lo son). (Figura 2b)

CONSTRUCCIÓN DEL ÁRBOL EXPRESIÓN.

Hasta ahora el resultado de todas las transformaciones que hemos realizado han sido cadenas de caracteres. El siguiente paso será obtener lo que se denomina el árbol expresión. Consiste en una estructura de datos con forma de árbol donde las hojas son operandos y el resto de hojas son operadores cuyos operandos son sus hijos.

El procedimiento para la generación del árbol expresión consiste en diferenciar en la cadena de caracteres el operador de mayor prioridad (que constituirá el nodo raíz del árbol expresión) y cada uno de sus operadores, (cosa que en nuestro caso será casi inmediato ya que hemos reescrito la expresión a notación postfija). Luego, para cada operador, repetiremos el proceso hasta que los operadores sean expresiones atómicas, entendiendo este tipo de expresiones como todo aquello que sea una constante o el identificador de una variable.

Como puede observarse se trata de un procedimiento recursivo, ya que cada operando de una expresión puede a su vez ser una subexpresión con un operador y uno o varios operandos, y así sucesivamente. Existen, básicamente dos variantes de dicho procedimiento dependiendo de en qué orden se hagan las llamadas recursivas. La primera se denomina width first (primero en anchura) y consiste en generar el árbol expresión por niveles de profundidad. Primero generamos el nodo raíz, luego sus hijos, luego los hijos de sus hijos, etc.

En el primer caso lo preferible es tener la cadena de caracteres en notación postfija. La segunda variante se denomina depth first (primero profundidad), consiste en generar el árbol de la siguiente manera: para cada nodo correspondiente a un operador binario analizaremos primero todo su subárbol derecho y después todo su subárbol izquierdo, y así sucesivamente.

En el siguiente caso lo preferible es tener la cadena de caracteres en notación prefija. La ventaja de este método sobre el anterior es que la cadena de caracteres sólo se lee una vez. La desventaja es que con el primer método es más fácil especificar qué errores sintácticos contiene la expresión, en el caso de que los haya, para poder informar al usuario. El algoritmo recursivo para generar el árbol expresión partiendo de la cadena expresión en notación postfija usando la variante width first puede concretarse en los siguientes pasos:

Algoritmo analizar_expresión:

- Paso 1. Llamaremos a analizar_subexpresión pasándole como parámetro la cadena introducida por el usuario.

- Paso 2. Devolver el puntero correspondiente al nodo raíz del árbol expresión. Terminar.
Algoritmo analizar_subexpresión:

- Paso 1. Leeremos el primer carácter. Si se trata de un '(' la subexpresión no es atómica. Es decir, tiene al menos un operador. Saltaremos al paso 2. En caso contrario leeremos toda la cadena y comprobaremos que corresponda a una constante numérica o al nombre de una variable. Crearemos un nodo hoja de la estructura árbol. En él almacenaremos el valor de la constante o el identificador de la variable (según corresponda) y devolveremos un puntero que apunte a dicho nodo. Terminar.

- Paso 2. Leeremos contando el número de '(' y de ')' hasta que los dos contadores valgan lo mismo y el siguiente carácter sea un ')' o una ','. Lo que hemos leído hasta ahora es el primer operando. Llamaremos a la función analizar_subexpresión pasando como parámetro la subcadena que hemos leído.

- Paso 3. Si el siguiente carácter era un ')' se trata de un operador unario. Saltar al Paso 6.

- Paso 4. Si por el contrario era una ',' se trata de un operador binario. Seguir.

- Paso 5. Seguiremos leyendo contando el número de '(' y de ')' hasta que los dos contadores valgan lo mismo y el siguiente carácter sea un ')'. Lo que hemos leído hasta ahora es el segundo operando. Llamaremos a la función analizar_subexpresión pasando como parámetro la subcadena correspondiente al segundo operando.

- Paso 6. Seguiremos leyendo hasta el final de la cadena. Lo que leemos es el operador. Comprobaremos que se trata de un operador válido y que hemos leído el número adecuado de operandos. Crearemos un nodo de la estructura árbol expresión. Haremos que los apuntadores de dicho nodo apunten a los subárboles resultantes de analizar los operandos y devolveremos un puntero que apunte al nodo del operador. Terminar.

vx = Bx - Ax
vy = By - Ay
vz = Bz - Az
ux = Cx - Ax
uy = Cy - Ay
uz = Cz - Az


     Æ  Æ  Æ


Æ i j k
vd = vx vy vz
ux uy uz
vdx = vy uz - vz uy
vdy = vz ux - vx uz
vdz = vx uy - vy ux


vdx vfx + vdy vfy + vdz vfz
Cos(a) = - v vdx 2 + vdx 2 + vdx 2
Porcentaje_luz = 100Cos(a)

Figura 6b. Fórmulas usadas para calcular el porcentaje de iluminación de una cara formada por los puntos A, B y C (en orden levogiro) con el vector característico del foco impropio (vf).

Si en cualquier momento de este proceso nos encontramos con el final de la cadena de caracteres, o con un carácter inesperado, retornaremos un código de error correspondiente y eliminaremos el árbol generado.

CALCULO DE VALORES DE LA FUNCIÓN

El objetivo de haber construido el árbol expresión es el de poder dar valores a las variables y evaluar la expresión para ver que el número que devuelve. Para ello usaremos un algoritmo que recorrerá el árbol en el mismo orden que el anterior lo generó.

Algoritmo calcular_expresión:

- Paso 1. Para cada nodo hoja correspondiente a una variable le asignaremos su valor actual.

- Paso 2. Llamar a calcular_subexpresión pasándole como parámetro el nodo raíz del árbol expresión.

- Paso 3. El valor que devuelve la función es precisamente el valor de la expresión. Retornar dicho valor y terminar.

Algoritmo calcular_subexpresión:

- Paso 1. Comprobaremos si el nodo pasado como parámetro es un nodo hoja. De ser así, retornaremos el valor que tenga asignado y terminaremos.

- Paso 2. En caso contrario estamos delante de un operador. Para cada uno de sus operandos (que estarán apuntados desde este nodo) llamamos a la función calcular_subexpresión pasando el operando como parámetro.

- Paso 3. Una vez que las llamadas recursivas devuelvan los valores de evaluar las subexpresiones correspondientes a los operandos, se operarán entre sí mediante el operador del nodo actual. Devolveremos el valor resultante y terminaremos.

Se podrían integrar los algoritmos (analizar_subexpresión y calcular_subexpresión) en uno, pero en nuestro caso no sería conveniente, ya que lo que queremos es generar una vez el árbol expresión y luego calcular muchos valores de dicha expresión sin tener que volver a analizarla. Como en nuestro caso las expresiones corresponden a funciones tridimensionales, las variables válidas serán la X y la Y, y el resultado de evaluar la expresión será la Z. Teniendo en cuenta que sólo hay dos variables válidas realizaremos la siguiente modificación del algoritmo analizar_subexpresión que hemos visto en el apartado anterior: tendremos dos nodos hoja, (uno para la X y otro para la Y) a donde apuntaran todos los operadores que los tengan como parámetros. De esta manera variando el campo donde almacenamos el valor de la X y la Y y ejecutando el algoritmo calcular_subexpresión iremos obteniendo los diferentes valores de la función que se va a representar. Esta modificación también puede resultar útil en el caso genérico.

PROYECCIÓN DE LA FUNCIÓN SOBRE EL PLANO

Llegados a este punto, ya somos capaces de analizar cualquier expresión matemática correspondiente a una función tridimensional introducida mediante el teclado por el usuario. También conocemos la forma de calcular valores de dicha función. El siguiente paso consistirá en representar dicha tabla de valores en pantalla. Para ello lo primero que hemos de considerar es el problema de la proyección de la función tridimensional sobre la pantalla (que sólo tiene dos dimensiones).

Función Input del programa
10*Sin(Sin(X)+Y)) ((((Y)Sin,X)+)Sin,10)*
Sin(X)*Cos(Y) ((X)Sin,(Y)Cos)*
· Sin(X*Y) (((X,Y)*)Sin),-1)*

Existen básicamente dos alternativas: usar la perspectiva cónico-frontal o usar una proyección plana. La primera tiene como ventaja que ofrece un efecto mucho más realista. Por el contrario, la segunda, al ser una vista en lugar de una perspectiva no distorsiona las proporciones. Como lo que queremos representar es una función matemática escogeremos la segunda alternativa.

Para pasar de las tres dimensiones a dos lo hacemos mediante un ángulo de caída y otro de deriva que el usuario podrá determinar

Para ver cómo se programaría la proyección en perspectiva y cómo realizar los giros de caída y deriva, deberán consultar otros números de la revista donde se haya tratado el tema.

REPRESENTACIÓN GRÁFICA EN "ALÁMBRICO"

Una vez que ya sabemos proyectar los puntos de la función sobre la pantalla podemos hacer una primera representación gráfica de la matriz de valores. Para ello tan sólo tendremos que decidir los intervalos de X e Y que configurarán el dominio de la función que vamos a representar. A continuación, se generará la matriz de valores y luego se trazarán líneas entre las coordenadas de pantalla de los puntos que sean adyacentes en la matriz de valores. Hemos decidido trazar una línea adicional 'la diagonal' para obtener 'caras' triangulares, ya que nos ira bien en apartados posteriores. Si bien se trata de una representación un tanto tosca, proporciona una imagen muy exacta de que apariencia que tiene la función. (Figura 5a) Cambiando el dominio y los ángulos de la proyección podremos ir obteniendo diferentes vistas de la función. Aunque, por supuesto, no vamos a contentarnos con representaciones alámbricas, en los siguientes apartados aprenderemos cómo incorporar un foco de luz para poder generar sombreados que den a las representaciones gráficas el efecto de relieve sólido.

ILUMINACIÓN Y FOCOS DE LUZ

Para poder iluminar una será necesario incorporar luz. Los objetos que emiten luz se denominan focos de luz, existiendo una gran variedad de ellos. Una de las muchas clasificaciones de focos de luz los divide en propios e impropios. Los primeros son aquellos que se encuentran a distancia finita del objeto que iluminar y radian en todas las direcciones. Los impropios son aquellos que se encuentran a una distancia infinita y por tanto se comportan como si sólo radiaran en una dirección. Como ejemplo intuitivo se puede pensar en los focos propios como si fueran bombillas y en los impropios como si fueran el sol. Por tanto los focos de luz propios vendrán caracterizados por una posición en el espacio y los impropios por un vector.

Como estamos iluminando una función matemática, dicha iluminación debe servir para darnos más información sobre la orientación y la pendiente de sus diferentes zonas. Es por esto que elegimos un foco impropio en nuestro caso, ya que de este modo iluminará de forma uniforme toda la superficie de la representación. En nuestro caso concreto lo más intuitivo será permitir que el usuario pueda modificar la orientación de su vector característico desde el programa mediante los siguientes parámetros: ángulo sobre la superficie plana y ángulo de elevación. (Figura 6a) Con esta caracterización del vector del foco y la posición de los tres puntos de la matriz de valores que conforman cada una de las caras podemos calcular que porcentaje de iluminación incide sobre ellas.

EL USO DE GRADIENTES

Una vez ya sabemos qué porcentaje de luz ilumina cada cara podemos pintarla del color que le corresponda: es decir el color del foco reduciendo el parámetro de luminosidad en el porcentaje de luz incidente. Pero, ¿qué ocurre si la resolución en la que trabajamos sólo soporta un reducido número de colores simultáneos en pantalla?, por ejemplo, en una resolución con 16 colores. En ese caso no tendremos más remedio que recurrir al uso de los gradientes. Un gradiente es una textura o patrón de pixels de dos o más colores que intenta simular el color que tiene como componentes RGB la suma ponderada de las componentes RGB de los colores que lo componen. En nuestro caso usaremos gradientes bicolores, donde un color será el color del foco de luz y el otro será el negro (la oscuridad). El porcentaje de luz que simulará el gradiente será precisamente el porcentaje de pixels del color del foco de luz respecto del número total de pixels. Para aprovechar ciertas subrutinas, que podemos encontrar en las librerías gráficas de buena parte de los compiladores de lenguaje de alto nivel, usaremos gradientes de 8x8 pixels. Cada gradiente tendrá dos pixels coloreados de diferencia con sus adyacentes. En concreto, como los gradientes serán de 64 pixels, usaremos 33 gradientes. Los gradientes se confeccionarán de tal forma que se provoquen los mínimos efectos laterales posibles. (Ver los gradientes del programa de ejemplo). Los efectos laterales se producen cuando el gradiente no está bien distribuido y al poner varios patrones, uno al lado de otro, se observan como 'cenefas' de pixeles.

REPRESENTACIÓN GRÁFICA EN "SÓLIDO"

Llegados a este punto ya sabemos calcular, para cada cara de la matriz de valores, el porcentaje de luz que le proporciona el foco impropio. Y por tanto, con qué gradiente hay que pintar cada cara. Podemos pues, proceder a la representación gráfica en "sólido". El algoritmo será prácticamente el mismo que en la representación en alámbrico, aunque hay un detalle que se deberá tener en cuenta, es muy probable que alguna cara se solape total o parcialmente con otra. Para evitarlo podemos usar el conocido algoritmo del pintor. Lo cierto es que este algoritmo es muy poco adecuado para la mayoría de las representaciones gráficas de mundos tridimensionales. En mundos donde hay mucho número de caras es costoso ordenarlas y pintar muchas caras que al final quedarán ocultas. Sin embargo nuestro caso es diferente, puesto que ya conocemos el orden en que hay que pintar las caras. Además, no habrá muchas caras que queden ocultas del todo y no tenemos demasiadas caras que pintar. El orden en que hay que pintar las caras es el siguiente: se empieza por la esquina más lejana de la matriz de valores y se recorre la matriz hacia su segunda esquina más lejana. Se va repitiendo el proceso hasta que se acaban las filas de la matriz.

EL PROGRAMA DE EJEMPLO

El artículo viene acompañado de un programa de ejemplo (llamado func-3d) para que no se quede todo en conceptos teóricos. Dicho programa permite al usuario introducir la función que representar en notación postfija (con paréntesis y comas). Una vez efectuado este paso tendremos que especificar el dominio que queremos ver, la orientación del foco de luz, la perspectiva, la proporción de eje Z respecto del X y el Y; y configurar los colores. (De hecho todas las capturas de pantalla que aparecen en el artículo corresponden al programa en cuestión).

Los operandos permitidos son: números, X, Y, Pi y e. Y los operadores permitidos son: + (suma), - (resta), * (multiplicación), / (división), Abs (valor absoluto), ArcTan (arcotangente), Cos (coseno), Ln (logaritmo neperiano), Sin (seno), Sqrt (raíz cuadrada) y Tan (tangente). Además se incluye, en la representación en alámbrico, la posibilidad de que el color de los trazos dependa del valor de la componente Z (usando los colores del arco iris).

El código del programa está escrito en Pascal con lo cual ningún programador experimentado tendrá problemas para traducirlo a su lenguaje preferido. El programa se puede compilar sin ningún problema en el compilador de Borland 'Turbo Pascal' (versión 6 o superior). No obstante se acompaña también el fichero ejecutable (que deberá copiarse junto con el fichero 'egavga.bgi' y los ficheros con extensión '*.chr' para que funcione).

Los requisitos de sistema para ejecutarse son mínimos: Funcionará en cualquier ordenador (286 o superior) con DOS. Como siempre, se anima al lector a que explore el código fuente y haga modificaciones para terminar de comprender los conceptos que en el artículo se explican.

Los módulos del programa

Todo el código fuente del programa está en un único fichero que incorpora los siguientes procedimientos y funciones. Todo aquel que desee analizar el código correspondiente al programa de representación gráfica de funciones, podrá encontrarlo en el CD-ROM de la revista.

Ejecutando el programa

Para probar el programa copiaremos el fichero ejecutable func-3d.exe, el fichero egavga.bgi y los ficheros con extensión chr en un mismo directorio y ejecutaremos. aparecerá una portada de presentación. Pulsaremos cualquier tecla y pasaremos al menú principal del programa. (Figura 8a) Una vez allí podemos pulsar la tecla 1 y ver la representación en alambrico de la función por defecto (-Sin(X*Y)); o pulsar 2 y ver la representación en sólido. Luego si queremos podemos modificar la orientación del foco de luz pulsando el 5 o los ángulos de caída y deriva de la proyección pulsando 6. Pulsando el número 3 cambiamos la función, (en la Figura 8b vemos algunas sugerencias) y pulsando el 4 cambiamos de dominio. Cuando cambiemos la función o el dominio a representar tendremos que tener cuidado de elegir una proporción de Z adecuada (opción 7 del menú principal), ya que si la función obtiene valores muy extremos la representación se saldrá de la pantalla. (Sin duda, un punto que mejorar en el programa) La opción 8 (configurar colores) sirve para elegir con que color se traza la función, los márgenes, etc. Si nos liamos mucho y queremos volver a los valores por defecto, bastará con pulsar la tecla 9. Y para salir basta con pulsar la tecla de escape en el menú principal.

Tabla 1. Cálculo de valores de la función.

- FUNCTION TECLAS: Lee una tecla del teclado.

- PROCEDURE FINESTRA: Dibuja una ventana con un gradiente determinado.

- PROCEDURE NUM_OUT: Imprime por pantalla un número.

- FUNCTION NUM_IN: Obtiene un número procedente del teclado.

- PROCEDURE MANEL: Dibuja la 'portada'.

- PROCEDURE CrearExpresión: Genera el árbol expresión.

- FUNCTION CALCULA_SUBEXPRESIO: Función recursiva para calcular el valor de un subárbol expresión.

- FUNCTION CALCULA_EXPRESIO: Función que calcula el valor del árbol expresión para una 'X' y una 'Y' dadas.

- PROCEDURE CALCULAR_COLOR: Determina a qué color del arco iris corresponde un punto de la función

- PROCEDURE CALCULAR_Z: Si no está generado el árbol expresión llama a 'CALCULA_EXPRESIO'.

- PROCEDURE INTRODUIR_CANTONADES: Cada vez que se cambia el dominio de la función recalcula los valores de las esquinas de la matriz de valores.

- PROCEDURE CALCULAR_MARGE: Calcula la proyección XY de un margen de la matriz de valores.

- PROCEDURE CALCULAR_MATRIU: Calcula la proyección XY de la matriz de valores.

- PROCEDURE CALCULAR: Controla mediante 'flags' que parámetros deben recalcularse.

- PROCEDURE QUI_PRIMER: Mira por que esquina debe empezar el algoritmo del pintor.

- PROCEDURE PRIMER: Traza el primer margen de la matriz.

- PROCEDURE DIBUIXAR_QUADRAT(P:BOOLEAN): Dibuja la proyección XY de los márgenes de la función.

- PROCEDURE DIBUIXAR_MATRIU: Traza toda la matriz.

- PROCEDURE PINTAR_TRIANGLE: Pinta con el gradiente adecuado una cara de la matriz.

- PROCEDURE PINTAR_DIBUIX: Pinta toda la matriz.

- PROCEDURE CANVIAR_FUNCIO: Permite al usuario cambiar la función

- PROCEDURE CANVIAR_DOMINI: Permite cambiar el dominio 'X' e 'Y' de la área a graficar.

- PROCEDURE APUNTA_FOCUS: Subrutina usada por 'CANVIAR_FOCUS'.

- PROCEDURE CANVIAR_FOCUS: Permite cambiar la orientación del foco de luz.

- PROCEDURE CANVIAR_PERSPECT: Permite cambiar la orientación de la vista determinando los ángulos de caída y deriva.

- FUNCTION PROPOR_Z: Para cambiar la proporción del eje 'Z' respecto los ejes 'X' e 'Y'.

- FUNCTION CONFIGCOLOR: Para configurar que colores se usarán.

- PROCEDURE DADES_INICIALS: Reasigna a todas las variables sus valores por defecto.

- PROCEDURE FI: Finaliza la ejecución del programa.

- CUERPO PRINCIPAL: Donde se muestra el menú principal.


Banner 468x60
Explorer 4.0, Netscape 4.0. Resolución 800 x 600.
©Tower Communications 1.998.
Diseño: GRUPO ALBERTINA DE COMUNICACION.