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. |