Visualizaci´on creativa de polinomios Proyecto #1 de Programaci´on Num´erica Marzo 2015 1 Introducci´ on Existen un sinf´ın de aplicaciones donde las matem´aticas y el c´omputo se combinan en aplicaciones cuyo u ´nico objetivo es generar un producto creativo (y en algunos casos, art´ıstico). Ejemplos de esto son: paisajes generados mediante fractales, animaci´ on por computadora (demoscene), videojuegos, m´ usica algor´ıtmica y otros (a los nefitos se les sugiere hacer una b´ usqueda en youtube de cada uno de estos ejemplos). Adem´as de proporcionar un medio de expresi´on, este tipo de aplicaciones permiten a los desarrolladores y mejorar sus habilidades matem´ aticas y de programaci´on. En este proyecto se pretende introducir al alumno a la animaci´on por computadora en tiempo real y al arte digital a trav´es de la visualizaci´on de polinomios en el plano complejo. Los objetivos particulares del proyecto son: • Que el alumno aprenda a desarrollar sus propias librer´ıas en lenguajes C y C++. • Que el alumno aprenda a desarrollar proyectos compuestos de m´ ultiples archivos fuente bajo Code::Blocks. • Que el alumno implemente una librer´ıa para el manejo b´asico de polinomios. • Que el alumno implemente una librer´ıa para visualizar cualquier funci´on de C a C. • Que el alumno utilice las librer´ıas desarrolladas para generar animaciones en tiempo real. 2 Implementaci´ on de librer´ıas en C/C++ Antes de comenzar formalmente el proyecto, es importante conocer c´omo se desarrollan proyectos de C/C++ un poco mas ambiciosos que los ejemplos usualmente vistos en clase. Un proyecto consiste generalmente de m´ ultiples archivos de c´ odigo fuente, donde solo uno de ellos contiene a la funci´on principal main(). Dado que en el curso se utiliza el lenguaje C++, todos los archivos fuente deben 1 llevar la extensi´ on .cpp y por lo general se debe tratar de que cada archivo fuente contenga funciones relacionadas en t´erminos de su funcionalidad. Por ejemplo, se puede tener un archivo fuente que contenga las funciones para el manejo de polinomios, otro que contenga funciones para trabajar con matrices, otro que tenga funciones para trabajar con im´agenes, etc. Incluso los archivos fuente que son demasiado extensos se pueden separar en dos o mas archivos. Para poder utilizar las funciones definidas en un archivo fuente dentro de otro es necesario escribir un archivo separado que contenga los encabezados de tales funciones y cualquier otra definici´on que sea necesario compartir con otros archivos fuente (e.g., definici´on de constantes, nuevos tipos de datos y clases). Este archivo de encabezados suele tener el mismo nombre que el archivo fuente correspondiente, pero con extensi´on .h o .hpp (esta u ´ltima solamente para c´ odigo en C++). El archivo de encabezado entonces se puede incluir dentro de cualquier archivo fuente mediante la directiva #include "nombre_del_archivo". Por ejemplo, suponga que se desea elaborar una librer´ıa con los m´etodos de obtenci´ on de ra´ıces vistos en clase. El archivo de encabezado (llam´emosle raices.h) puede quedar como sigue: // Archivo raices.h #ifndef _raices_h #define _raices_h double biseccion(double (*f)(double x), double a, double b, double precision = 1e-10, bool info = false) ; double falsapos(double (*f)(double x), double a, double b, double precision = 1e-10, bool info = false); double puntofijo(double (*g)(double x), double p, int maxit = 100, double precision = 1e-10, bool info = false); double newtonraphson(double (*f)(double x), double (*df)(double x), double xr_ant, int maxit = 100, double precision = 1e-10, bool info = false); #endif El c´ odigo que se encuentra entre las directivas #ifndef y #endif se compilar´ a solamente si la macro _raices_h no ha sido definida. Ya que lo primero que se hace en este caso es definir la macro, eso impedir´a que el compilador trate de compilar el c´ odigo m´as de una vez (lo cual provocar´ıa un error de compilaci´ on), a´ un cuando el archivo de encabezado se incluya m´ ultiples veces en otros archivos fuente. Adem´ as de este archivo, es necesario escribir la implementaci´on de las funciones en un archivo que posiblemente se pueda llamar raices.cpp. Este ejercicio se le deja al alumno (quien en teor´ıa ya debe haberlas implementado). 2 Una vez escritos los archivos raices.cpp y raices.h es posible integrarlos en cualquier programa. Por ejemplo, a manera de prueba podemos escribir el siguiente programa en otro archivo main.cpp: // Archivo main.cpp #include <iostream> #include "raices.h" using namespace std; double f(double x) { return cos(3 * x) - x; } int main() { double raiz; raiz = biseccion(f, -2, 1, 0.2, true); cout << endl << "La solucion del examen es: " << raiz << endl; return 0; } Ahora solo necesitamos buscar la manera de poder compilar ambos archivos fuente y generar un solo archivo ejecutable. Normalmente esto requiere compilar cada archivo fuente por separado (lo cual genera un archivo objeto con extensi´ on .o por cada archivo fuente) y luego enlazar todos los archivos objeto en un solo ejecutable. Afortunadamente los entornos de programaci´on como Code::Blocks y Xcode permiten la creaci´on de proyectos, los cuales realizan el proceso de compilaci´ on y enlace autom´aticamente. Para crear un proyecto con Code::Blocks hay que seguir los pasos siguientes: 1. Iniciar Code::Blocks 2. Elegir la opci´ on New-¿Project del men´ u File, o bien la opci´on Create a new project en la pantalla de inicio. 3. Elegir la opci´ on Console application y posteriormente el lenguaje C++. 4. Ingresar un nombre para el proyecto y elegir la carpeta donde se guardar´an los archivos del proyecto. 5. Utilizar los valores por defecto para el resto de las opciones. 6. Una vez creado el proyecto, en la barra del lado izquierdo aparece un ´arbol (llamado Workspace) que muestra los archivos que conforman el proyecto. Inicialmente el u ´nico archivo es main.cpp, el cual contiene una versi´on del programa Hola Mundo a manera de ejemplo. 7. Para verificar que el proyecto se puede compilar junto con la librer´ıa SDL, agregue la siguiente l´ınea al inicio de main.cpp #include <SDL2/SDL.h> 3 y modifique el encabezado de la funci´on main de la manera siguiente: int main(int argc, char *argv[]) Si SDL se encuentra correctamente instalado, el programa debe compilar y mostrar el mensaje Hello world! en la pantalla. 8. Para agregar un archivo fuente al proyecto, puede seleccionar la opci´on New-¿Empty file del men´ u File (o simplemente teclear Ctrl-Shift-N). Code::Blocks le preguntar´ a si desea agregar el archivo al proyecto (ind´ıquele que s´ı) y posteriormente se le pedir´a el nombre del archivo, el cual por lo general ser´ a un archivo fuente (con extensi´on .cpp) o un archivo de encabezado (con extensi´ on .h). 9. Tambi´en es posible agregar al proyecto archivos de c´odigo ya existentes. Esto puede hacerse a trav´es de la opci´on Add files... del men´ u Project, o bien, haciendo click derecho en el nombre del proyecto dentro del ´arbol Workspace y seleccionando la opci´on Add files... del men´ u contextual. 10. Para respaldar el proyecto, copie toda la carpeta donde eligi´o almacenar el proyecto. 3 Librer´ıa para el manejo de polinomios La primer etapa del proyecto consiste en implementar una librer´ıa (un archivo fuente con su correspondiente archivo de encabezado) para el manejo b´asico de polinomios. Esta librer´ıa despu´es ser´a extendida para la b´ usqueda de ra´ıces, pero por lo pronto solo se requerir´an las funciones b´asicas vistas hasta ahora en clase. Un polinomio es una funci´on p(x) que tiene la siguiente forma: p(x) = a0 xn + a1 xn−1 + a2 xn−2 + . . . + an−1 x + an , donde en general tanto los coeficientes aj , j = 0, . . . , n y la variable x pueden ser complejos. Todo polinomio de grado n est´a completamente determinado por sus n + 1 coeficientes (aunque algunos de ellos sean iguales a cero). Por lo tanto, podemos representar un polinomio mediante un vector de n´ umeros complejos. Como se vi´ o en clase, en C++ podemos utilizar las clases complex y vector para trabajar con este tipo de datos. De hecho, se sugiere definir un nuevo tipo de datos (utilizando typedef) que consista en un vector de complejos: typedef vector<complex<double>> polinomio; La clase vector funciona de manera muy parecida a un arreglo, en el sentido de que uno puede acceder a sus elementos de manera aleatoria mediante el operador de sub´ındice [] (recordando siempre que los sub´ındices inician desde cero hasta el tama˜ no del arreglo menos uno), pero cuenta adem´as con funcionalidad 4 adicional que nos permite, entre otras cosas, conocer el tama˜ no del vector o modificarlo, y agregar elementos al final del vector (increment´ando de manera din´ amica su tama˜ no). Inicialmente solo requeriremos tres funciones: una para asignar los valores de los coeficientes del polinomio, otra para imprimir un polinomio en la pantalla, y una m´ as para evaluar el polinomio en cualquier valor complejo. Estas funciones ya se han visto en clase. En particular, las dos primeras han sido implementadas a trav´es de la sobrecarga del operador <<, de manera que se puede imprimir el polinomio directamente mediante cout, y tambi´en se pueden agregar los coeficientes de una manera simple y compacta. A continuaci´ on se presenta un ejemplo de c´omo podr´ıa quedar el archivo de encabezado para las funciones de polinomios. Para este ejemplo hemos elegido el nombre polinomio.h: // Archivo polinomio.h #ifndef _polinomio_h #define _polinomio_h #include <iostream> #include <vector> #include <complex> using namespace std; // Se define un polinomio como un vector de coeficientes complejos typedef vector<complex<double>> polinomio; // operador para agregar coeficientes a un polinomio polinomio &operator <<(polinomio &p, complex<double> c); // operador para enviar polinomios a un flujo de salida (e.g., cout) ostream &operator <<(ostream &os, polinomio &p); // funcion para evaluar un polinomio complex<double> evalua_poli(polinomio &p, complex<double> x); #endif Nuevamente se deja al alumno el ejercicio de crear el archivo de implementaci´ on correspondiente polinomio.cpp (las tres funciones ya fueron discutidas en clase), as´ı como un programa de prueba (e.g., main.cpp) para verificar que las tres funciones operan correctamente. 5 4 Librer´ıa para visualizar una funci´ on compleja Por el momento, esta librer´ıa consistir´a u ´nicamente de una sola funci´on; sin embargo, para desarrollar esta funci´on se requiere comprender algunos conceptos que se estudian en el curso de Programaci´on Avanzada: apuntadores, arreglos bidimensionales en orden lexicogr´afico y clases. En esta secci´on se presentan primero cada uno de estos conceptos de forma breve (a manera de repaso), junto con algunos ejercicios, y luego se procede a describir el procedimiento para visualizar una funci´ on compleja. 4.1 Apuntadores La memoria de una computadora suele estar organizada como un arreglo de bytes (donde cada byte equivale a un elemento de tipo char en lenguaje C/C++), donde a cada byte le corresponde un sub´ındice. Todas las variables que se usan en un programa est´ an almacenadas en alg´ un lugar de la memoria, pero pueden ocupar un n´ umero distinto de bytes (este n´ umero es el tama˜ no de la variable). Por ejemplo, existen variables con tama˜ nos de 1 byte (e.g. char), de 2 bytes (e.g., short), de 4 bytes (e.g., int y float), de 8 bytes (e.g., double) y otros. Una variable siempre ocupa bytes consecutivos en la memoria. Pensando que la memoria es un arreglo de bytes, es posible conocer la posici´on dentro de este arreglo donde se ubica el primer byte de una variable mediante el operador &. A esta posici´ on se le conoce como la direcci´ on de memoria de la variable (o simplemente la direcci´ on de la variable). Tambi´en es posible conocer el tama˜ no de una variable mediante el operador sizeof. Por ejemplo, examine la salida del siguiente programa: int main() { char c; short s; int i; float f; double d; cout << "Direccion de cout << "Tama~no de c cout << "Direccion de cout << "Tama~no de s cout << "Direccion de cout << "Tama~no de i cout << "Direccion de cout << "Tama~no de f cout << "Direccion de cout << "Tama~no de d return 0; c = s = i = f = d = = " = " = " = " = " " << &c << endl; << sizeof(c) << endl; " << &s << endl; << sizeof(s) << endl; " << &i << endl; << sizeof(i) << endl; " << &f << endl; << sizeof(f) << endl; " << &d << endl; << sizeof(d) << endl; } 6 Observe que las direcciones de memoria se imprimen en sistema hexadecimal. Podemos hacer que se impriman en decimal mediante un cambio de tipo (typecasting) a long. Por ejemplo: cout << "Direccion de c = " << (long)&c << endl; Es posible guardar la direcci´on de memoria de una variable en otra variable. A las variables que pueden contener direcciones de memoria se les llama apuntadores. Uno puede declarar un apuntador precediendo el nombre de la variable con el operador *. Ejemplo: int x = 1; int *p = &x; *p = 2; cout << x << endl; En este ejemplo, x es una variable de tipo int, mientras que p es un apuntador a int, el cual se inicializa con la direcci´on de x. Es decir, la variable p contiene la posici´ on en la memoria en donde se ubica la variable x. Por lo general, todo apuntador est´a asociado a un tipo de datos, el tipo de datos al que apunta. En el ejemplo anterior p puede almacenar la direcci´on de memoria de cualquier variable de tipo int, pero no la de una variable de tipo float o char (a menos que forcemos al compilador a hacerlo). Una vez que tenemos un apuntador a una variable, podemos acceder al valor de esa variable (ya sea para recuperarlo o modificarlo) nuevamente mediante el operador *. Por ejemplo, *p representa el valor de la variable a la que p apunta. Se puede asignar el valor NULL a un apuntador para indicar que ´este no apunta a ninguna variable en particular. De hecho, uno siempre debe asignar a un apuntador una direcci´ on v´alida de memoria (donde realmente se encuentre una variable del tipo adecuado), o bien, incializar el apuntador con NULL. Por otra parte, algunas funciones que devuelven apuntadores puede ser que devuelvan el valor NULL para indicar que una operaci´on no exitosa. Num´ericamente la constante NULL siempre equivale a cero. 4.2 Arreglos bidimensionales en orden lexicogr´ afico En muchas ocasiones es necesario elaborar programas que puedan manejar datos organizados en forma de una matriz. Un ejemplo claro es el manejo de im´agenes, ya que una imagen digital se representa como una matriz con un cierto n´ umero de renglones y un cierto n´ umero de columnas, donde cada celda de la matriz representa un pixel en la imagen. Una matriz puede verse como un arreglo en donde se requiere de dos sub´ındices (n´ umero de rengl´on y columna) para ubicar cada elemento, en lugar de un solo sub´ındice como en los arreglos comunes. Afortunadamente, es posible utilizar un arreglo sencillo (de un solo sub´ındice) para representar los datos de una matriz si seguimos una cierta convenci´on, que consiste en almacenar los datos de la matriz por renglones; es decir, primero los datos del primer rengl´ on, luego los del segundo, etc. Esta convenci´on se 7 conoce como orden lexicogr´ afico, ya que es similar al orden que seguimos al leer y escribir. Por ejemplo, supongamos que se tiene una matriz de M renglones y N columnas (es decir, de M × N ) y deseamos almacenarla en un arreglo unidimensional. Ya que la matriz tiene M N celdas, entonces el tama˜ no del arreglo ser´a tambi´en M N , y los sub´ındices de los elementos del arreglo van de cero a M N − 1. Usando la convenci´ on anterior, el sub´ındice dentro del arreglo que le corresponde a cada elemento de la matriz es el siguiente: 0 1 2 ... N −1 N N +1 N +2 ... N + (N − 1) 2N 2N + 1 2N + 2 . . . 2N + (N − 1) . .. .. .. . . . . . . . . . (M − 1)N (M − 1)N + 1 (M − 1)N + 2 . . . (M − 1)N + (N − 1) No es dif´ıcil ver que a la celda que se localiza en el rengl´on y y la columna x le corresponde el sub´ındice yN + x (donde N es el n´ umero de columnas en la matriz), pensando que los renglones est´an numerados de 0 a M − 1 y las columnas de 0 a N − 1. 4.3 Arreglos y apuntadores En los lenguajes C y C++ existe una relaci´on intr´ınseca entre arreglos y apuntadores. En primer lugar, todos los elementos de un arreglo se localizan en un bloque contiguo de memoria, ordenados en el mismo orden en que aparecen en el arreglo. Por ejemplo, considere el siguiente arreglo: int x[10]; Cada elemento del arreglo es de tipo int y por lo tanto ocupa 4 bytes en la memoria. Por lo tanto, todo el arreglo ocupa 40 bytes. Ahora, supongamos que la direcci´ on del primer elemento del arreglo, x[0], es la direcci´on 1000. Entonces, la direcci´ on de x[1] ser´a 1004, la de x[2] ser´a 1008, y as´ı. Es claro que podemos obtener la direcci´on de cualquier elemento del arreglo sumando a la direcci´ on del primer elemento un m´ ultiplo del tama˜ no de los elementos del arreglo. En otras palabras, la direcci´on de x[i] es igual a la direcci´on de x[0] mas i veces el tama˜ no de cada elemento. De hecho, la operaci´on anterior se realiza en un programa cada vez que se accede a un elemento del arreglo. Esto se verifica f´ acilmente con un peque˜ no programa: int main() { int x[10]; int i; for (i = 0; i < 10; i++) { cout << "Direccion de x[" << i << "] = "; cout << (long)&x[i] << endl; } 8 return 0; } Por otra parte, el nombre de un arreglo (sin el sub´ındice) representa un apuntador al primer elemento del arreglo. Es decir x equivale a &x[0] para cualquier arreglo x. Finalmente, es posible sumar una cantidad entera n a un apuntador; en este caso, el compilador asume que el apuntador contiene la direcci´on de alg´ un elemento de un arreglo, y el resultado de la operaci´on ser´a la direcci´on del elemento que se ubica n posiciones despu´es en el arreglo. En pocas palabras, si x es un arreglo, entonces la direcci´on del elemento x[n] se puede obtener tambi´en como x + n, independientemente del tipo de datos de los elementos del arreglo. De manera similar, si se tiene un apuntador a un elemento del arreglo, incrementar el apuntador en uno har´a que apunte al siguiente elemento del arreglo. 4.4 Clases Los arreglos permiten organizar datos que son todos del mismo tipo. Sin embargo, en muchas ocasiones uno requiere organizar o agrupar datos de distintos tipos. En lenguaje C++ uno puede agrupar datos de distintos tipos mediante la definici´ on de una clase. Una clase es un tipo de datos compuesto, que puede comprender m´ ultiples variables de distintos tipos a las que se puede acceder de manera individual. Por ejemplo, suponga que dentro de nuesto programa necesitamos la informaci´ on de una cierta regi´on rectangular en el plano complejo. Com´ unmente se define un rect´angulo especificando dos puntos en el plano, los cuales corresponden a esquinas opuestas del rect´angulo. Sin embargo, para esta aplicaci´ on definiremos un rect´angulo a trav´es de su centro (dado como un punto en el plano complejo), su anchura y su altura (ambas n´ umeros reales). Adem´as, podemos pensar que los lados de un rect´angulo no necesariamente deben ser paralelos a los ejes, por lo que agregaremos una propiedad m´as que sera la orientaci´ on del rect´ angulo (dada como un ´angulo en radianes). La siguiente figura sirve como referencia de las cuatro propiedades de nuestro rect´angulo: el centro es el punto rojo, la anchura y altura son las longitudes de las l´ıneas verdes, y la orientaci´ on es el ´ angulo que hace la l´ınea de la anchura con el eje real (mostrado en la figura en color azul como el ´angulo α). 9 Si queremos manipular un rect´angulo como ´este dentro de un programa, tendr´ıa mos que definir las cuatro propiedades como variables; por ejemplo: complex<double> centro; double ancho; double alto; double orientacion; Todo esto est´ a muy bien si nuestro programa solo manejara un rect´angulo, pero es posible que querramos hacer un programa que trabaje simult´aneamente con dos, tres, diez, o cualquier n´ umero arbitrario de rect´angulos. Por otra parte, cada funci´ on que requiera la informaci´on de un rect´angulo deber´a recibir como argumentos las cuatro propiedades del rect´angulo, mas los argumentos adicionales que la funci´ on requiera. Muy pronto nos dar´ımos cuenta de lo impr´ actico que resulta programar de esta manera. Una mejor soluci´ on consiste en definir una clase que contenga como elementos las cuatro propiedades de un rect´angulo. Esto podemos hacerlo de la siguiente forma: class RRect { public: complex<double> centro; double ancho; double alto; double orientacion; }; Es importante no olvidar escribir public: y el punto y coma al final de la definici´ on de la clase. Los elementos que forman la clase (en este caso el centro, ancho, alto y orientaci´ on) se llaman miembros de la clase. Una clase funciona como un nuevo tipo de datos, por lo tanto podemos declarar variables de este nuevo tipo de datos como lo har´ımos para las variables de cualquier otro tipo. Las variables cuyo tipo de datos es una clase se llaman objetos. Por ejemplo, la sentencia RRect r1, r2; sirve para declarar dos objetos de clase RRect. Los objetos se llaman r1 y r2, y cada uno de ellos cuenta con su propio centro, ancho, alto y orientaci´on. Para acceder a las propiedades individuales de cada rect´angulo se utiliza el operador punto. Por ejemplo, si queremos que r1 sea un cuadrado unitario centrado en el origen, con los lados paralelos a los ejes, entonces escribir´ımos lo siguiente: r1.centro = 0; r1.ancho = 1; r1.alto = 1; r1.orientacion = 0; 10 Otro aspecto interesante de las clases es que algunos de sus miembros pueden ser funciones. A las funciones que son miembros de una clase se les llama m´etodos y por lo general se utilizan para manipular los miembros de datos de esa clase. En este proyecto no utilizaremos m´etodos, excepto por un m´etodo especial que se denomina constructor y que permite inicializar los miembros de datos de la clase. Un constructor siempre lleva el mismo nombre que la clase a la que pertenece y no devuelve ning´ un resultado (ni siquiera void). Como ejemplo, a continuaci´ on se presenta la definici´on completa de la clase RRect incluyendo su constructor: class RRect { public: complex<double> centro; double ancho; double alto; double orientacion; // constructor de la clase RRect(complex<double> c=complex<double>(0,0), double an=1, double al=1, double o=0) : centro(c), ancho(an), alto(al), orientacion(o) {} }; El constructor de RRect permite inicializar un rect´angulo con los par´ametros que se especifiquen a la hora de declararlo; si los par´ametros se omiten, el rect´ angulo se inicializa como un cuadrado unitario centrado en el origen con lados paralelos a los ejes. De esta manera, podemos ahora declarar objetos de clase RRect e inicializarlos en ese momento. Por ejemplo: RRect r1, r2(complex<double>(1,-1), 3, 2, M_PI / 4); En este ejemplo r1 nuevamente ser´ıa un cuadrado unitario centrado en el origen con lados paralelos a los ejes, mientras que r2 es un rect´angulo con un tama˜ no de 3 × 2 unidades centrado en el punto 1 − i y una orientaci´on de 45◦ . A estas alturas es posible que ya se haya dado cuenta de que los tipos de datos complex y vector en realidad son clases, y que por ejemplo, la funci´on size() es un m´etodo de la clase vector. Uno puede declarar tambi´en apuntadores a objetos de una cierta clase. Si tenemos un apuntador a un objeto, podemos acceder a los miembros de ese objeto a trav´es del apuntador mediante el operador ->. Por ejemplo: RRect r1; RRect *p = &r1; cout << "centro = " << p->centro << endl; cout << "tamano = " << p->ancho << " x " << p->alto << endl; cout << "orientacion = " << p->orientacion * 180 / M_PI << " grados" << endl; 11 4.5 Superficies de SDL En SDL una imagen se representa mediante una clase llamada SDL_Surface (en realidad no es una clase, pero ese es un detalle que podemos omitir). SDL_Surface tiene muchos miembros pero los mas importantes son: int w; int h; int pitch; void *pixels; // // // // anchura de la imagen en pixels (numero de columnas) altura de la imagen en pixels (numero de renglones) numero de bytes que ocupa cada renglon apuntador al arreglo de datos A grandes rasgos, una imagen es similar a una matriz. En el caso de SDL_Surface, el miembro w representa el n´ umero de columnas de la matriz, que corresponde a la anchura de la imagen, el miembro h es el n´ umero de renglones de la matriz o la altura de la imagen, y el miembro pixels es un apuntador al primer elemento del arreglo que contiene los datos de la matriz o imagen, en orden lexicogr´ afico. Sin embargo, hay dos detalles importantes: el primero es que pixels es un apuntador tipo void, es decir, un apuntador que no est´a asociado a ning´ un tipo de datos, por lo que se requerir´a hacer un typecast para convertirlo en un apuntador al tipo de datos adecuado (en nuestro caso, este tipo ser´ a un entero sin signo de 32 bits llamado Uint32). El segundo detalle es que, por cuestiones de eficiencia, el n´ umero de bytes utilizados para almacenar cada rengl´ on de la imagen puede ser mayor al requerido (es decir, mayor a w * sizeof(Uint32)). Por lo tanto, para acceder al pixel (x, y) (en la columna x y rengl´ on y) debemos tomar el miembro pixels y convertirlo a un apuntador a Uint8 (o cualquier otro tipo de datos de 8 bits), luego sumarle la cantidad necesaria para desplazarse al inicio del rengl´on y y convertir el resultado en un apuntador a Uint32, y finalmente sumarle x para desplazarse al pixel deseado. Podemos, por ejemplo, escribir una funci´on para cambiar el color de un pixel en una imagen: void setpixel(SDL_Surface *s, int x, int y, int r, int g, int b) { // Calcula la direccion del pixel (x, y) en la imagen a la que s apunta Uint32 *pixel = (Uint32 *)((Uint8 *)s->pixels + y * s->pitch) + x; *pixel = (r << 16) | (g << 8) | b; } Note que s es un apuntador a un objeto SDL_Surface, por lo que para acceder a sus miembros se requiere usar el operador -> (e.g., s->pixels, s->pitch, etc). Cada elemento de la matriz representa el color del pixel correspondiente como un entero de 32 bits, donde los 8 bits menos significativos contienen el valor de la componente azul, los siguientes 8 bits el de la componente verde, y los siguientes 8 bits el de la componente roja. Dado que la intensidad de cada componente de color se representa con 8 bits, el rango de valores es de 0 a 255. 12 5 Visualizaci´ on de un polinomio El objetivo principal de este proyecto es poder visualizar un polinomio en forma de una imagen. Dado que la variable independiente del polinomio es compleja, entonces podemos tratar de visualizar el comportamiento del polinomio en una regi´ on rectangular del plano complejo (como la que definimos en la clase RRect). El primer problema consiste en encontrar un mapeo que relacione la posici´on (x, y) ∈ Z 2 de un pixel en una imagen con un punto z ∈ C dentro de un rect´ angulo en el plano complejo, tal como se observa en la siguiente figura: Consideremos un objeto llamado rect de clase RRect y un apuntador a un objeto de clase SDL_Surface llamado img. El ancho de la imagen es img->w, mientras que el ancho del rect´angulo complejo es rect.ancho. Consideremos tambi´en el caso mas simple, cuando la orientaci´on del rect´angulo es cero y sus lados son paralelos a los ejes. Supongamos que sabemos que el pixel (x, y) en la imagen se corresponde con el punto complejo z en el rect´angulo. Entonces, el pixel (x+1, y) en la imagen se corresponder´a con el punto complejo z+dx, donde dx = rect.ancho / (img->w - 1)+0i. Es decir, dx es un vector que indica cu´ anto hay que desplazarse en el rect´angulo por cada desplazamiento de un pixel a lo largo de un rengl´ on de la imagen. Similarmente, por cada desplazamiento de un pixel a lo largo de una columna en la imagen, el punto z en el rect´angulo se desplazar´ a en la direcci´ on dy = 0−(rect.alto / (img->h - 1))i. Note tanto dx como dy son complejos (de manera que puedan sumarse directamente a z) y que en dy se usa un signo negativo - esto u ´ltimo es debido a que en una imagen los valores de y crecen hacia abajo, pero en el plano complejo la parte imaginaria crece hacia arriba. Ya que sabemos c´ omo desplazarnos de un punto del rect´angulo a otro, ahora solo requerimos encontrar un punto inicial z0 . La elecci´on mas sensata es la esquina superior izquierda del rect´angulo, la cual se puede encontrar simplemente como z0 = rect.center - dx * img->w / 2 - dy * img->h / 2 Una vez calculados dx, dy y z0 , se realiza un simple doble ciclo para recorrer la imagen y dibujar cada pixel: for (y = 0; y < img->h; y++) { 13 Uint32 *pixel = (Uint32 *)((Uint8 *)img->pixels + y * img->pitch); z = z0; for (x = 0; x < img->w; x++) { pixel[x] = colorea(evalua_poli(p, z)); z += dx; } z0 += dy; } donde p es un objeto de clase polinomio y colorea es una funci´on que transforma un valor complejo a un color RGB dado como un n´ umero entero de 32 bits. La imagen resultante depender´a en gran medida de la manera en que colorea asocie valores complejos con colores. Por ejemplo, uno puede obtener cada componente RGB como el producto punto entre el complejo z y un vector complejo asignado a cada componente de color: double punto(complex<double> a, complex<double> b) { return a.real() * b.real() + a.imag() * b.imag(); } int clip(int x) { return ((x >= 0) && (x <= 255)) ? x : ((x < 0) ? 0 : 255); } Uint32 colorea1(complex<double> &z, void *param) { Uint32 r, g, b; kr = complex<double>(1, 0); kg = complex<double>(0, 1); kb = comple<double>(-1, 1); r = (Uint32)clip(abs((int)dot(z, kr))); g = (Uint32)clip(abs((int)dot(z, kg))); b = (Uint32)clip(abs((int)dot(z, kb))); return (r << 16) | (g << 8) | b; } Finalmente, podemos f´ acilmente generalizar a los casos donde la orientaci´on del rect´ angulo en el plano complejo es distinta de cero. Para esto, simplemente hay que rotar los vectores de desplazamiento dx y dy, lo cual puede lograrse al multiplicarlos por la constante exp −iφ, donde φ es la orientaci´on del rect´angulo. Se sugiere implementar una funci´on que tome como argumentos un apuntador a una superficie SDL, una referencia a un RRect, una referencia a un polinomio, y cualquier otro argumento necesario, para visualizar una regi´on del polinomio en la imagen. 14 6 Animaci´ on Dependiendo de la velocidad de la computadora, el tama˜ no de la imagen y la eficiencia del c´ odigo, es posible que se pueda generar una imagen en una peque˜ na fracci´ on de segundo. En estos casos, uno puede programar animaciones en tiempo real generando y mostrando las im´agenes de manera continua, y alterando ligeramente los par´ametros en cada cuadro. Entre los par´ametros que se pueden alterar est´ an la posici´on, orientaci´on y dimensiones de la regi´on rectangular del plano complejo que se est´a visualizando, los coeficientes del polinomio, los par´ ametros de la colorizaci´on, etc. Se recomienda al estudiante experimentar para encontrar animaciones interesantes. El siguiente c´ odigo muestra c´omo puede implementarse el ciclo de animaci´on usando SDL. En este caso, el ciclo termina en el momento en que el usuario presiona una tecla cuando la ventana de SDL tiene el foco: clock_t t0; long frames = 0; double t, spc = 1.0 / CLOCKS_PER_SEC; // spc = segundos por tick del reloj SDL_Event event; SDL_Surface *frame = SDL_GetWindowSurface(win); t0 = clock(); do { t = (clock() - t0) * spc; // t = tiempo transcurrido en segundos // modificar parametros de visualizacion aqui RenderizaPolinomio(frame, rect, p); // funcion para visualizar SDL_UpdateWindowSurface(win); // Muestra la imagen en la ventana frames++; SDL_PollEvent(&event); // detecta eventos del usuario } while (event.type != SDL_KEYDOWN); 15
© Copyright 2024