Proyecto #1

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