TP3: Escena 3D Sebastián SANTISI, 82.069 [email protected] 1er. Cuatrimestre de 2007 66.71 Sistemas Gráficos Facultad de Ingenierı́a, Universidad de Buenos Aires Resumen En el presente trabajo se desarrolló una aplicación tridimensional completa en OpenGL, modelando la xilografı́a Relativiteit de M. C. Escher. En la misma el usuario es capaz de moverse por la escena, en modo de primera persona, interactuando con el escenario. Índice 1. Introducción 3 2. Relativiteit 2.1. La obra . . . . 2.2. Las gravedades 2.3. La perspectiva . 2.4. Lo plasmado . . . . . . 4 4 5 5 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3. Desarrollos algorı́tmicos 3.1. Polı́gonos cóncavos, PyPolygon2tri 3.1.1. Ear cutting . . . . . . . . . 3.1.2. Implementación . . . . . . . 3.1.3. Bugs conocidos . . . . . . . 3.2. Escenarios . . . . . . . . . . . . . . 3.3. Movimientos en primera persona . . 3.4. Texturas . . . . . . . . . . . . . . . 3.5. Sólidos de revolución . . . . . . . . 3.6. Sólidos de extrusión . . . . . . . . . 3.7. Escena . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 7 7 7 8 9 10 11 12 13 13 4. Trabajo final 4.1. Clases . . . . . . . . . . . 4.1.1. keyboard . . . . . 4.1.2. poly tri . . . . . 4.1.3. bezier y bspline 4.1.4. converter . . . . . 4.1.5. vector . . . . . . . 4.1.6. gravity . . . . . . 4.1.7. first person . . . 4.1.8. textures . . . . . 4.1.9. revolution x 3 . . 4.1.10. personaje . . . . . 4.1.11. extrusion . . . . . 4.1.12. scene . . . . . . . 4.1.13. stars . . . . . . . 4.1.14. tp3 . . . . . . . . . 4.2. Iluminación . . . . . . . . 4.3. Funcionamiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 15 15 15 15 15 16 16 16 16 17 17 17 17 18 19 20 20 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5. Trabajo a futuro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 2 1. Introducción Este informe corresponde a la documentación del trabajo llevado adelante para concretar la construcción de un programa capaz de modelar en tres dimensiones la xilografı́a Relativiteit de M. C. Escher. Encararemos la construcción del proyecto desde tres perspectivas diferentes El análisis de la obra, donde describiremos las diferentes técnicas que se aplicaron sobre los originales, para poder definir el tipo de herramientas a utilizar en la construcción, de manera que fuera viable desarrollar la escena. El análisis de las herramientas desarrolladas, para poder concretar el armado final del programa. La descripción del programa, con sus respectivas clases, interfases y jerarquı́as funcionales; y, también, el uso del programa desde el punto de vista del usuario. A diferencia de en otras entregas realizadas para esta materia, esta vez no se ahondará en todo el detalle que harı́a falta para explicar correctamente todo lo desarrollado. Apenas se entablará una conversación a modo de paneo general, para poder explicar cómo se realizó la construcción. Se toma esta decisión por la gran extensión que tendrı́a un informe que llevara cuenta del detalle. Realmente, se desarrollaron muchos conceptos, estrategias y herramientas para poder llegar a este trabajo final. 3 http://img407.imageshack.us/img407/4096/escherrelativitywoodcutdf9.jpg Figura 2.1: Relativiteit (Relatividad), M. C. Escher, 1953. Grabado sobre madera, 11”x12”. 2. Relativiteit Relativiteit (Relatividad) es un famoso grabado (figura 2.1), realizado en 1953 por el holandés Maurits Cornelis Escher (1898–1972). Cronologicamente con el desarrollo del trabajo, empezaremos por el análisis completo que se hizo sobre la obra y los fundamentos para el diseño de la aplicación desarrollada. 2.1. La obra La obra madura de Escher se caracteriza por la búsqueda de la paradoja matemática, de la ilusión, de lo imposible. Su obsesión radica en la repetición, la fusión y en explorar de diferentes maneras las fallas que tiene el modelado bidimensional de escenas tridimensionales. Escher se caracterizó por ser un excelente ilustrador; pero en su obra tardı́a, la ilustración es sólo el medio para evidenciar sus absurdos lógicos. Se quiso rendir un homenaje a Escher desde la realización del presente trabajo; por lo que se eligió una de sus obras para ser llevada al modelado 3D. De entre todas las obras de M. C. Escher, se eligió Relativiteit. La elección de dicha obra se centró en que es una de las pocas obras de su carrera que muestran la ilusión del punto de vista de la perspectiva, pero que, además, representan una escena tridimensional real; es decir, mientras que muchas de las obras de Escher representan absurdos conseguidos con puntos de vista forzados, el absurdo de Relativiteit está dado por lo subjetivo del observador. Si bien esta no es la única obra de Escher que juega con la subjetividad de la observación, pareció la más rica para explotar desde diferentes perspectivas como una integración de los contenidos del curso de Sistemas Gráficos. Se eligió la versión xilográfica, la cual tal vez no es la más conocida de las dos que realizó Escher, por considerar que era posible realizar una reproducción fiel de los cortes en la madera, los cuales son mucho más geométricos que los de la versión en litografı́a de la obra. Relativiteit representa una conjunción de tres mundos en un único espacio. Cada mundo pende de una gravedad distinta y los tres coexisten ajenos unos de otros en las escaleras del mismo edificio. Cada personaje pertenece a una de estas gravedades y no puede escapar de ella; circulan abstraidos por la suya y parecen no reparar en las demás. Siguiendo la obra de Escher, es evidente que la paradoja filosófica de Relativiteit es la inversa a la de otras de sus obras que giran en torno a espacios de Möebius. Desde el punto de vista teórico de la perspectiva, juega a confundir el cénit con el nadir; hecho que se ve agravado por la existencia de tres puntos de fuga y ninguna dirección predominante en la escena o un punto de fuga central que prioricen una orientación. 4 http://img46.imageshack.us/img46/906/pyopengleschersrelativiak5.png Figura 2.2: Análisis de los diferentes planos de la obra. 2.2. Las gravedades El primer trabajo que se realizó sobre la xilografı́a, fue el de intentar separar de alguna manera las gravedades, para poder realizar un relevamiento coherente del espacio ocupado por los diferentes elementos. Para poder distinguir claramente a las gravedades, se pintó de un color diferente a cada plano según su gravedad (figura 2.2). Más tarde, al realizar la escena, se construyó a la misma respetando la coloración original del primer acercamiento a la obra; por lo que este primer análisis influyó bastante en el resultado final de la escena. En la escena coloreada se realzaron un montón de detalles que en la escena monocroma estaban velados, permitiendo entender realmente la precisión del juego de los tres mundos. En base a la observación de la misma se comenzaron a esbozar y poner a prueba las herramientas que conformaron el desarrollo final. Las observaciones más importantes fueron dos: Sólidos de extrusión. La escena completa podı́a descomponerse en sólidos de revolución1 , cada uno de ellos atado generalmente a una dirección unı́voca. A partir de este temprano hallazgo se evaluó que era viable el describir a la escena desde el bajo nivel, sin recurrir a herramientas de modelado 3D; la descripción se realizarı́a definiendo perfiles y profundidades de extrusión. Cada gravedad como mapa de alturas. Salvo pocas excepciones, el espacio transitable por el personaje de una gravedad no se superponı́a en diferentes niveles. Es decir, el escenario podrı́a ser pensado como curvas de nivel del espacio tridimensional con mı́nimas intersecciones entre niveles; un caso apenas más complejo que el de viejos juegos tridimensionales en escenarios tridimensionales, como por ejemplo Doom. Este descubrimiento harı́a posible el poder permitir la navegación libre por la escena de personajes de diferentes gravedades; simplemente bastarı́a con escribir el mapa de alturas de cada gravedad. En base a estas observaciones, se comenzó a construir un modelo que pudiera describir la realidad del escenario. 2.3. La perspectiva Con la tesis de la viabilidad de los sólidos de extrusión demostrada, comenzó a ser necesario relevar en detalle la escena completa. Para ello se procedió a “desarmar” la perspectiva para obtener las dimensiones y proporciones del escenario. El método aplicado para deshacer la perspectiva aplicado fue el inverso al aplicado al plasmar un volumen en un plano bidimensional (figura 2.3). Se buscaron 1 Si bien la mı́nima división podrı́a haber sido el paralelepı́pedo, el hallazgo fue el haber encontrado una estructura más compleja que cuajara con la escena. 5 http://img254.imageshack.us/img254/4539/pyopengleschersrelativing0.png Figura 2.3: Lı́neas guia aplicadas realizando el camino inverso de la generación de la vista en perspectiva. http://img526.imageshack.us/img526/1782/pyopengleschersrelativiee5.png Figura 2.4: Vista final del programa en modo perspectiva de cámara esférica. los puntos de fuga de las direcciones principales, y mediante muchas lineas auxiliares, medidores, intersecciones de diferentes planos ortogonales, etc.; fue lográndose relacionar las diferentes posiciones de los puntos de interés hasta poder completar la escena completa. Se aplicó la misma neurosis obsesiva de Escher (o más) para lograr obtener proporciones fieles a las del modelo original. Del muestreo de la perspectiva fue importante el descubrir que la métrica del dibujo es el escalón. Utilizando ese patrón, todas las alzadas y pedadas son del mismo tamaño, y los anchos son medidas enteras, al igual como la mayor parte de los objetos; apenas un par de objetos puntuales se escapan a esto. Del aprovechamiento de esta singularidad enunciada, se desprendió la unidad de representación del dibujo y se pudieron simplificar muchas variables asociadas al movimiento del observador en el dibujo. 2.4. Lo plasmado El trabajo final reproduce con exactitud los edificios visibles en la escena (figura 2.4). Salvo el haber obviado detalles de los marcos y puertas, también se han reproducido con exactitud cada una de las texturas del grabado original. Originalmente, la escena incluı́a a los personajes que se observan en la xilografı́a, pero luego, por acotar el trabajo, fueron retirados de la misma; si bien está realizado parte del desarrollo y estuvieron presentes en demostraciones tempranas del proyecto. En la vista externa a la gravedad, se muestra completo el edificio original, con agregados que repiten las paredes inconclusas del grabado, para llevar toda la escena a un volumen cúbico. No se representaron las vistas del paisaje exterior. En la vista en primera persona se completaron varias paredes y jardines para no dejar al observador mirando a precipicios. Se completó el ambiente con un cielo detallado, que quita la sensación de vacı́o de la recorrida contra un fondo negro. 6 3. Desarrollos algorı́tmicos Mientras se construı́a la escena, fue haciéndose necesario el desarrollo de diversas herramientas y mecanismos que fueron dándole soporte al proyecto que emergı́a. Se intentará en esta sección describir con diversos grados de detalles las herramientas desarrolladas. 3.1. Polı́gonos cóncavos, PyPolygon2tri Al comenzar a desarrollar escaleras representadas como extrusiones de polı́gonos, la primera limitación contra la que se chocó en OpenGL fue debido a la incapacidad de dicha biblioteca de renderizar polı́gonos cóncavos. Se intentó utilizar la utilidad GluTesselator pero fue evidente que la misma está “rota” en los paquetes de OpenGL (no ası́ de Mesa). Luego de intentar infructosamente por horas encontrar una manera de hacer funcionar las funciones de la familia de GluTess* bajo OpenGL, se decidió implementar algoritmos de triangulación de polı́gonos. El primer intento de salvar el problema consistió en buscar implementaciones de triangulaciones en internet. Se encontraron realmente muy pocas y en diversos lenguajes de programación. Sin comprender su funcionamiento, se realizó la traducción a Python de todas ellas. Lamentablemente ninguna de las bibliotecas funcionó renderizando sin errores polı́gonos de las caracterı́sticas de los que se tenı́an en el trabajo. El paso siguiente fue leer bastantes trabajos y papers centrados en la triangulación de polı́gonos para poder evaluar y comprender cuál era el funcionamiento de los algoritmos aplicables y cuál convenı́a para el caso desarrollado. Dado que los sólidos no tenı́an huecos, y dado que la cantidad de vértices no era muy grande y que para el perfil de escalones era probable que no se necesitaran muchas iteraciones para encontrar un triángulo, se determinó que la solución más razonable era aplicando el algoritmo de ear cutting. 3.1.1. Ear cutting El algoritmo de ear cutting es un tı́pico algoritmo ávido. El mismo consiste en buscar orejas en el polı́gono, dibujarlas y retirarlas del mismo, dando lugar a un nuevo polı́gono. Una oreja es el triángulo formado por tres vértices consecutivos los cuales no tengan ningún otro vértice en su interior. Un teorema, facilmente demostrable de manera gráfica, garantiza que todo polı́gono tiene al menos dos orejas. Si bien el orden de complejidad de ear cutting, en el peor caso, es evidentemente cúbico; el mejor caso es cuadrático. La configuración de los polı́gonos del trabajo, al consistir en escalones, garantiza un orden practicamente cuadrático. 3.1.2. Implementación La implementación final, la cual se liberó bajo el nombre de PyPolygon2tri (o poly tri.py); está basada en la implementación poly tri.c de Reid Judd y Scott R. Nelson de 1988. 7 Como ya se dijo en un primer momento, todos los intentos por traducir a Python las implementaciones ya existentes, evidenciaron defectos en las mismas por lo que los polı́gonos de nuestro problema no fueron triangulables. Con los elementos adquiridos luego de leer la teorı́a del tema, fue posible corregir los bugs de las versiones anteriores. Se eligió esta versión por la sencillez, eficiencia y por su carga histórica. El algoritmo básico está implementado como Input: Polı́gono Orientación ←− OrientaciónDe(Polı́gono); while Longitud(Polı́gono) > 3 do foreach Vértice in Polı́gono do Triángulo ←− TresVérticesConsecutivosA(Vértice); if OrientaciónDe(Triángulo) = Orientación then if EsOreja(Polı́gono, Triángulo) then Imprimir(Triángulo); Eliminar(Polı́gono, Vértice); end end end end Imprimir(Polı́gono); El cálculo de la orientación se obtiene sumando el tamaño (con signo) del producto vectorial de cada vértice consecutivo (igual a dos veces el area del triángulo que forman con el origen). De este modo, es claro observar que las aristas que avanzan de manera horaria mirando desde el origen dan un aporte positivo y las antihorarias uno negativo. Si la suma de los aportes negativos es mayor que la suma de los aportes positivos, entonces el polı́gono gira en sentido antihorario. Si sumara positivo, entonces gira en sentido horario. Para cada conjunto de vértices consecutivos; si los mismos tuvieran el mismo sentido de giro del polı́gono, entonces su area está orientada hacia el interior del mismo. Caso contrario, no contienen al polı́gono y no pueden constituir una oreja. Para ser una oreja, además de estar orientada hacia el interior, no debe contener más puntos; si cumple las dos condiciones, entonces se renderiza ese triángulo y se retira del polı́gono la oreja, es decir, se elimina el vértice interior. La implementación se vale de una función callback para realizar la impresión, por lo que es independiente del trabajo sobre OpenGL. 3.1.3. Bugs conocidos Si bien la implementación trabaja bien con una gran variedad de formas cóncavas, no se pudo determinar la causa por la que falla con perfiles que tienen concavidades en todas sus caras. Se supone que hay un error de redondeo de signos en determinantes cercanos a 0.0, por lo que no se está detectando correctamente la inclusión de puntos dentro de un triángulo. 8 El resultado de este problema es que se consideran orejas aristas con puntos interiores y se rasterizan llenas concavidades. Se espera poder corregir el bug a futuro; pero no afecta al trabajo presente dado que se redefinieron las listas de puntos para evitar el disparo del error. 3.2. Escenarios Como se dijo anteriormente, el trabajo permite el desplazamiento del observador dentro de cada una de las tres gravedades de la escena. Para poder realizar esto, fue necesario el describir mapas que indicaran las alturas de los diferentes pisos, los obstáculos y los lı́mites del espacio. Para el desarrollo de una implementación sencilla se explotaron las dos peculiaridades que se describieron en las secciones 2.2 y 2.3; la escasa superposición de niveles dentro de una misma gravedad, y la métrica unitaria de toda la escena, respectivamente. El hecho de que la métrica fuera unitaria, determinó la descripción de los escenarios según una suerte de bitmaps; sabiendo que podrı́an aplicarse coordenadas discretas para representar el nivel de un area y que los niveles posibles estarı́an muy acotados al ser enteros. El hecho de que la superposición fuera poca, hizo que con sólo dos mapas por gravedad fuera posible describir la totalidad de la escena. Los archivos descriptores se desarrollaron en texto plano, en formato ASCII (al igual que los demás descriptores del trabajo). Los mismos consisten en un encabezado, seguido de varias matrices, cuyas celdas representan un area cuadrada de un escalón de lado, codificando un número en base 36 (0..9-A..Z) o un guión (-) para indicar la ausencia de valor, el cual indica la altura de la misma. El mapa se traduce en un mapa tridimensional mapeado sobre coordenadas (x, y). El encabezado son tres lı́neas numéricas en las cuales el primer valor representa el offset en x, el segundo valor el offset en y y el tercer valor el offset en la altura (z); luego una lı́nea en blanco. Las matrices describen valores sobre x en sus columnas y sobre y en sus filas. La posición de arriba a la izquierda de la misma marca la coordenada expresada por los offsets dados en el encabezado. El valor de cada celda es ajustado según el offset en z. Cada matriz se separa por una lı́nea en blanco, y a la siguiente se le aplican las mismas reglas que a la primera. Por ejemplo, el mapa 3 7 5 89 BA CD -E representa a una espiral que empieza en la coordenada (3, 7), a la altura de 13 (510 + 836 ), y termina en la coordenada (4, 8), después de haber dado más de una 9 vuelta, a la altura 19 (510 + E36 ). En la coordenada (4, 7) hay dos posibles alturas, 14 o 18; mientras que en la coordenada (3, 8) sólo es posible la altura 16. En memoria, este mapa se representa como diccionarios de diccionarios de listas, en donde diccionariox,y es la lista de alturas posibles para la coordenada x, y. 3.3. Movimientos en primera persona Al comienzo de la animación del movimiento en primera persona se hizo evidente que no era natural el movimiento dado por pasos discretos al activar los controles. Se trató de implementar un modelo de movimiento que emulara de una manera más realista el movimiento natural de un cuerpo al recibir estı́mulos. La solución que se desarrolló, es el desarrollo de un sistema que responde a los estı́mulos, y entrega una señal que indica cuánto hay que moverse. Si bien el enfoque matemático fue más bien intuitivo en las primeras instancias del desarrollo, el mismo fue hecho a consciencia. La función de movimiento responde a un sistema LTI causal el cual acumula estı́mulos individuales en el momento en el que los mismos se originan. Cada pedido de movimiento dispara una respuesta al impulso discreta que está formada por un muestreo de una decena de puntos del segundo polinomio cuadrático de Bernstein, multiplicado por una cierta constante; es decir, una señal con impulso inicial que luego se apacigua. El desarrollo guarda un sistema para el avance y otro sistema para la rotación angular. En cada instante de tiempo se rota lo que diga la señal de rotación y se avanza en el sentido actual lo dictado por la señal de avance. Los estı́mulos tienen signos, por lo que un estı́mulo hacia adelante puede ser contrarrestado por un impulso hacia atrás, o el mantener constante el estı́mulo en un sentido hace que se alcance una velocidad de movimiento constante. Además de la noción de posición, sentido y los sistemas que describen la evolución futura; el sistema de primera persona tiene una instancia de un escenario (sección 3.2. En base a ese escenario, se sabe si es posible avanzar en el sentido indicado. Cuando se alcanza un lı́mite del escenario, o se está ante un obstáculo/abismo insalvable (sólo se puede pasar a niveles que no disten en más de una unidad de altura), el movimiento puede seguir produciéndose unidimensionalmente, si una de las dos coordenadas todavı́a tiene libertad. Es decir, si se llega a una pared de manera oblicua, el avanzar actualizará aún la coordenada perpendicular a la pared, en la misma cantidad que si la pared no estuviera. Se implementó esto tardı́amente para hacer mucho más aceitada la navegación y evitar que el personaje se estanque. Pese a que los mapas del escenario representan un area de un escalón cuadrado, y esta es la medida entera de todo; se guarda un coeficiente de 0,2 en el cual no se puede avanzar si fuera de ese lı́mite no hay continuidad con el piso que se transita. Esta restricción se toma porque la restricción de clipping mayor a cero de OpenGL causarı́a que las paredes atravesaran el cuadro al llegar a una de ellas de manera oblicua sin este coeficiente de seguridad. 10 Fue necesario, también; imponer una velocidad máxima, la cual está fijada en ≈ 0,8, es decir un valor tal que nunca se pueda dar un paso que sobrepase más de una unidad del mapa de alturas. Si no estuviera esta restricción, el personaje podrı́a atravesar paredes; además de que se frenarı́a en las escaleras al no poder subir dos escalones por paso. El sistema completo exporta dos coordenadas, una de posición y otra de dirección de vista; y consta con cinco funciones, una para computar el siguiente paso y otras para avanzar, retroceder, girar a derecha y girar a izquierda. La implementación del sistema se realiza sobre una especie de cola. Cada actualización de la posición desencola un valor de la misma y modifica la posición; en caso de estar la cola vacı́a, asume haber sacado un valor nulo. Cada estı́mulo le suma el muestreo de la respuesta al impulso a los valores presentes en la cola, y encola valores nuevos de ser la cola más corta que el muestreo. La cola tiene su tamaño acotado por la cantidad de puntos en el muestreo del impulso. 3.4. Texturas Para hacer lo más compatible posible a la aplicación con instalaciones genéricas de Python2 , se intentó prescindir de bibliotecas de manejo de imágenes, como puede serlo Python Image Library (PIL). Al igual que con los mapas de escenarios, se desarrolló un pequeño formato descriptor ASCII para images, en un formato análogo al raw. Un archivo de texturas es una secuencia de texturas separadas por renglones. Cada textura se compone de un encabezado y de su información. El encabezado consta de 4 lı́neas; la primera es el nombre que identificará a la textura, la segunda es el ancho de la misma, la tercera el alto, y la cuarta el formato. La textura está codificada en ASCII, y se implementaron dos formatos diferentes, según la necesidad del presente trabajo. Extender los formatos a más es practicamente trivial. Dado que se trabajó con imágenes monocromáticas, se implementaron modos acordes a eso. Los formatos desarrollados fueron dos 8 bits: El formato de 8 bits, describe una imagen en modo raw, en la cual cada dos bytes ASCIIs se puede leer un número hexadecimal que representa el valor de color de ese pixel. La lectura se hace de a un pixel por vez, empezando desde la esquina superior izquierda y avanzando primero hacia derecha y luego hacia abajo, según el ancho y alto especificado. 4 bits: El formato de 4 bits fue pensado como una simplificación del de 8 (si bien el formato de 4 es cronológicamente anterior en el desarrollo), cada byte ASCII es un dı́gito de un número hexadecimal el cual se duplica para obtener el valor de 8 bits correspondiente. Es decir, el byte 5 representa al número 5516 , es decir, al decimal 85; esta compresión de representaciones está inspirada en 2 Esto, desde ya es un imposible, dado que se hace uso del paquete PyOpenGL, el cual tiene varias dependencias; pero de todos modos, se buscó no agravar esa situación. 11 otras especificaciones, como por ejemplo CSS, y tiene la ventaja de representar 16 colores distintos, en el rango 0..255, con poco espacio. Se utilizó este formato porque los mapas monocromos fueron tipeados a mano y eran más sencillos de esta forma; también se podrı́a haber utilizado codificación PGM, pero hubiera resultado menos versatil a futuro. También se desarrollaron herramientas (estas sı́ sobre PIL), para convertir imágenes al lenguaje descriptor desarrollado. 3.5. Sólidos de revolución Como parte del desarrollo inconcluso de los personajes de la obra de Escher, se plantearon distintos tipos de sólidos de revolución. Si bien en el trabajo final no se terminó usando ninguno más que el más sencillo. Los tres sólidos que se incluyeron en los códigos entregados, más allá de los que se descartaron, son Revolución simple: Es la revolución tradicional. Se recibe una lista de puntos de control de Bézier o BSpline, bidimensionales, (x, y). Se computa la rotación de los valores de y a lo largo del eje x. Revolución elı́ptica: Esta revolución toma dos perfiles; con coordenadas (x0 , y0 ) y (x1 , y1 ). Computa la rotación de los valores de las yes a lo largo de los valores dados por x0 . En el plano yz se aplican los valores (y0 cos θ, y1 sin θ); es decir, se generan perfiles elı́pticos de coeficientes y0 e y1 . Es importante remarcar que las coordenadas en x están dadas sólo por uno de los dos perfiles; ası́ que es de esperar que hayan deformaciones si no son similares los dos perfiles entregados. Revolución doble-elı́ptica: Esta revolución toma tres perfiles, y genera un sólido con simetrı́a bilateral en base a ellos. Análogamente a como se computa la revolucón elı́ptica, esta revolución toma las equis del primer perfil y pondera según la fórmula de ella para 0 < θ < π; para valores de π < θ < 2π utiliza los valores de y del tercer perfil; por lo que se obtiene una sección formada por dos hemielipses contı́nuas entre sı́, con el parámetro y1 común a ambas. En el camino hasta llegar a la ponderación elı́ptica pasaron diferentes intentos, como ponderaciones lineales, por Bernstein (la lineal, en realidad, son un caso particular de Bernstein con grado 2), cuadráticas, ¡y hasta según parametrizaciones de espirales! Al dar con las parametrizaciones elı́pticas se consiguió lograr cuerpos de revolución con continuidad C 2 , que era lo que se buscaba al experimentar con diferentes ponderaciones. La implementación final, para ajustarla a lo pedido, opera sobre matrices; las implementaciones iniciales hacı́an el cálculo de los quads, de las normales y el dibujado on-the-fly. La generación mediante matrices no sólo no complejizó sino que redujo drasticamente la resolución de los algoritmos. Para mayor eficiencia, se generan las matrices en dos pasadas; la primer matriz es la matriz de puntos, y la segunda matriz se calcula en base a la primera con las 12 normales de cada punto. Las normales de las columnas exteriores se calculan contra la fila siguiente; mientras que las normales interiores se calculan interpolando la fila anterior y siguiente a la fila a la que pertenece el punto. Esto hace que cada punto tenga una normal diferente y elimina practicamente por completo el facetado en el sólido final. Las ventajas de hacer el cálculo de normales en una segunda pasada son varias; en primer lugar, no hay un crecimiento de complejidad dado que, si bien la implementación en dos pasadas, se conserva el orden; en segundo lugar, se gana muchı́simo en la suavidad del sólido, dado que se dispone de muchı́sima más información que la que es razonable llevar en una implementación en una pasada; y, en tercer lugar, es importante que se puede omitir la generación de la matriz de normales si no importa renderizar una versión sólida del objeto. Desde el punto de vista del diseño, la implementación del cálculo de puntos y de normalización disociada hizo posible una herencia muy sencilla entre las tres clases. La clase que genera los sólidos de revolución simples tiene escrita la lógica principal; las clases de sólidos de revolución elı́ptica y doble-elı́ptica apenas reescriben las 5 lı́neas del algoritmo generador de puntos, dejándole el resto a los métodos heredados de la clase base. La interfase de instanciación de un sólido de revolución contiene los parámetros de posición y dirección, necesarios para que por sı́ sola sea capaz de ubicar al objeto en su lugar definitivo. 3.6. Sólidos de extrusión La implementación de los sólidos de extrusión en sı́ es bastante sencilla y no aporta en mucho describirla. Los sólidos se generan para una de las tres dimensiones, in situ (es decir, no hay transformaciones asociadas); la decisión de implementarlos de esta manera, si bien poco versatil, parte de tratar de bajar la complejidad de la generación del escenario principal, siendo que su carga es recurrente en cualquier contexto de la aplicación. Los sólidos son ortogonales, y generan sus dos caras extruidas sobre planos paralelos. Está claro que la intención no fue, en ningún momento, la de desarrollar una biblioteca de funciones de extrusión genéricas y versátiles (además, para eso existe GLE). La generación de un sólido se hace a partir de un polı́gono que representa su contorno, y dos coordenadas que representan entre qué valores de la dirección debe realizarse la extrusión. También se entregan los nombres de las texturas a utilizar y el sentido de las mismas. Las texturas se proveen por cada plano cartesiano; es decir, dadas las caracterı́sticas del dibujo, se implementó que todas las caras cercanas al plano xy del sólido tuvieran la misma textura y color, y ası́ con los otros dos planos. 3.7. Escena La descripción completa de la escena, en términos de texturas, y sólidos se levanta de archivos que codifican los volúmenes y la manera de representarlos. El formato es, como cada vez en todo el desarrollo, un archivo ASCII de texto plano. Cronologicamente, la descripción de la escena, fue el primer formato que se diseñó, y el más coplejo. En algún momento se pensó en hacer el lenguaje des13 criptor compatible con PostScript, para poder visualizar con cualquier visor de archivos los perfiles, pero se abandonó pronto la idea. Eso sı́, es posible que, en un futuro cercano, se reescriba el formato descriptor y se tome de PostScript el funcionamiento como arquitectura de stack. El formato es una serie de extrusiones, separadas por lı́neas en blanco. Las lı́neas que comienzan con numeral (#) son comentarios. Cada comando o valor está separado de los demás por caracteres blancos. Cada extrusión tiene un encabezado y una secuencia de comandos. El encabezado es o x, o y o z y dos números; en base a eso se describe en qué dirección se realizará la extrusión y entre qué valores. Los comandos pueden ser Puntos: Un punto es una coordenada bidimensional expresada como x,y. Curvas de Bézier: Una curva de Bézier es el identificador bezier seguido de cuatro puntos, siendo ellos sus puntos de control. Escalones: Una secuencia de escalones se conforma por el comando steps, seguido opcionalmente de los modificadores down y/o left para indicar que la misma no es ascendente o hacia la derecha, respectivamente; seguido opcionalmente de los modificadores cutb y/o cute, los cuales indican que se debe recortar el primero o el último escalón; seguido del número de escalones, seguido de un punto que representa la coordenada de inicio. Texturas: Una declaración de texturas comienza con texture y es seguido por las declaraciones de textura para x, y y/o z, en ese orden. Cada declaración de textura es la letra del plano en el cual se aplicará la textura (se entiende por plano x al plano perpendicular a x; es decir el yz; y análogamente con los otros dos), seguida opcionalmente de la letra r, que indica que la textura debe ser rotada, seguida del nombre de la textura. Reverse: Indica que la lista se está describiendo en sentido antihorario. Esto es indiferente al usar PyPolygon2Tri (sección 3.1) dado que la clase de extrusión chequea el sentido de giro dado que “no le cuesta nada hacerlo”, y lo corrige antes de llamar a la función de recorte de orejas (a quien le avisa que ya no debe volver a chequer el sentido); pero en los sistemas en los cuales funcionan las herramientas de Tesselator de GLU, estas se utilizan, y pueden quedar normales apuntando hacia el interior del objeto de no revertirse las listas. El parser se encarga de ir concatenando en secuencia los puntos, escalones y curvas de Bézier. Con ellos y con los parámetros de textura fijados, si los hubiera, instancia un objeto sólido de extrusión por cada sólido descrito. Puede observarse como la descripción de la escena no incluye a los sólidos de revolución y superficies generadas con mallas; las mismas se encuentran hardcodeadas en el cargador de escena y se cargan si en el archivo descriptor están las lı́neas revolution o corniza. Esto es algo a ser corregido para próximas versiones; al igual que poder incluir la iluminación en el lenguaje descriptor. 14 4. Trabajo final Hasta el momento hemos descrito los principales conceptos y herramientas desarrollados para llegar a la concreción del trabajo. A partir de ahora describiremos algunos detalles técnicos de la implementación, si bien no entraremos en un detalle riguroso de los mismos. 4.1. Clases Si bien se empezó con el desarrollo del trabajo bastante antes de que el mismo tuviera un enuciado definido, el mismo tuvo una extensión considerable como para haber pasado por sucesivos clen-ups, y pese a eso seguir teniendo fallas de diseño importantes. Hay muchos huecos en la manera en la que quedaron armadas las jerarquı́as y es proyecto a futuro el racionalizar el diseño. Daremos una breve recorrida por cada uno de los módulos, su implementación y su funcionamiento; muchos de ellos ya fueron comentados en las secciones previas. Algunos de los módulos fueron desarrollados, pero no fueron usados en la versión que se presenta; sin embargo serán descritos porque formaron parte del desarrollo y serán integrados a futuro. 4.1.1. keyboard La clase keyboard es la que encapsula el manejo de los eventos de teclado. La documentación de la misma se encuentra en el TP2 de esta misma materia. 4.1.2. poly tri El módulo poly tri encapsula en la función draw poly la funcionalidad descrita en la sección 3.1. Esta función recibe una lista de vértices, un puntero a una función que toma 3 puntos y un argumento genérico, ese argumento genérico, y la orientación del polı́gono (para no recalcularla en caso de ya haberse hecho) y aplica el algoritmo de ear cutting sobre el polı́gono, llamando al callback con cada triángulo. 4.1.3. bezier y bspline Las clases bezier y bspline son dos clases estáticas las cuales devuelven un iterador o una evaluación de puntos, en base a una secuencia de puntos de control y una cantidad de puntos dadas. Las mismas llevan una caché de las evaluaciones sobre sus bases hechas en llamadas previas, por lo que sucesivas llamadas con la misma cantidad de pasos no necesitan recomputar los polinomios generadores. Para la clase que genera Bezier, de haber más de 4 puntos de control, los mismos se asumen como una curva continua iterando de a tres de ellos por vez. 4.1.4. converter Este módulo es una pequeño programa de consola el cual convierte una imagen pasada por argumento al formato de 8 bits de texturas (sección 3.4), “escupiendo” 15 el resultado de la conversión por stdout. Hace uso de las facilidades de la biblioteca PIL. 4.1.5. vector Si bien esta clase terminó sin usarse para reducir el overhead en la generación de vectores, y por no haber tantas aplicaciones en las cuales se justifica su uso, la misma está escrita completa y funcional. Se trata de una clase, muy sencilla, la cual encapsula aplicando distintas estrategias de cálculo-λ, toda la funcionalidad necesaria para operar con vectores bi o tridimensionales, representados como un tipo n-upla. Están implementadas todos los operadores y sobrecargas, y el tipo es indiferente para operar contra otros vectores o contra cualquier tipo de Python que sea representable como una secuencia (siempre y cuando coincidan las longitudes de ambos). 4.1.6. gravity La clase gravity contiene la implementación del parseo de escenarios explicado en la sección 3.2. Una instancia de la clase se crea desde un nombre de archivo y, luego, puede ser invocada con una coordenada instancia[x,y], llamada en la cual devuelve la lista asociada con la coordenada x, y o None, de no haber posibles valores de altura para esa posición. Se encuentran definidos los archivos gravity x, gravity y y gravity z, los tres de extesión .dat, que contienen los mapas de alturas para cada una de las tres gravedades de la escena. 4.1.7. first person La clase first person implementa lo descrito en la sección 3.3. Se instancia desde la ruta del mapa de alturas que va a respetar, y exporta los métodos y propiedades ya enumeradas. 4.1.8. textures La clase textures se encarga de levantar las texturas desde un archivo dado y de registrarlas ante OpenGL. Luego, la misma al ser invocada con el operador corchetes, devuelve el ı́ndice asignado por OpenGL para la textura del nombre indicado. La clase se encarga de habilitar los parámetros de texturas de OpenGL y, a menos que se cambie después, deja seteadas a las textudas según los filtros GL NEAREST para mag y GL LINEAR MIPMAP NEAREST para min. Se eligieron estos filtros dado que las texturas que se repiten en todas las paredes son monocromáticas (y, practicamente, unidimensionales); elegir oros filtros, para mag, hacı́a aparecer nuevos valores intermedios entre el blanco y el negro, y para min, se veı́a muy notoria la distancia al observador a partir de la cual cambiaba el buffer de profundidad asociado a la textura, viéndose franjas con diferentes texturas. Si bien la textura lineal, sumada a las rayas del diseño de la textura, se presta para un gran efecto 16 de aliasing, fue preferible dicho aliasing a los otros efectos colaterales de filtros suavizantes. La textura, por omisión, queda en modo GL OBJECT LINEAR; practicamente en toda la aplicación se usa esta estrategia, dado que la unicidad en la métrica de los objetos también se manifiesta en la métrica de la textura. Luego los objetos al dibujarse redefinen la orientación de GL OBJECT PLANE, pero no tienen la necesidad de setear cordenadas explı́citas dado la naturaleza de la escena. Se encuentra definido el archivo textures.dat, el cual contiene la descripción de las 4 texturas que utiliza la aplicación. 3 de ellas de 4 bits, monocromáticas, que representan los diferentes cortes de la xilografı́a. Y una cuarta, de 8 bits, en escala de grises (generada con la herramienta de conversión), la cual es una textura de la superficie lunar. Esta última textura consiste a la excepción en el tratamiento lineal, dado que se renderiza utilizando GL SPHERE MAP. 4.1.9. revolution x 3 Se proveen tres clases de revoluciones, las cuales ya fueron detalladas en la sección 3.5. Las mismas se encargan de generar, almacenar y renderizar sólidos de revolución, de revolución elı́ptica y de revolución doble-elı́ptica. 4.1.10. personaje La clase personaje no se entrega funcionando en esta entrega, la misma se encarga de renderizar completos a los hombrecitos de la escena, aplicando sucesivas transformaciones de rotación y translación según los ángulos de cada uno de sus miembros. La manera de resolver el dibujo es anidada. La rutina de dibujo del torso, llama a dibujarse a la cabeza, los brazos y los muslos, entregándole la coordenada origen en el lugar en el cual ellos tiene su pivote. Luego, la rutina de brazo dibuja su antebrazo y la del antebrazo la mano; y la rutina del muslo llama a la pierna y esta al pie. De este modo, se pueden representar a personajes en cualquier posición. Los volúmenes de los personajes están descritos con revoluciones doble-elı́pticas, para dar la apariencia de simetrı́a bilateral en los brazos, piernas, antebrazos, muslos, manos y pies; con revolución simple para el geoide de la cabeza; y con mallas de bezier (provistas por GL) para el torso. 4.1.11. extrusion La clase extrusion es la comentada en la sección 3.6, simplemente recibe una lista de puntos del perfil, los nombres de las texturas a aplicar, almacena al objeto textura actual como un atributo estático de su clase, y se encarga de dibujar los diferentes sólidos. 4.1.12. scene La clase scene implementa la carga de descripciones de escenas descritas en la sección 3.7. La misma procesa el archivo de descripción de la escenas, genera los 17 http://img45.imageshack.us/img45/9899/pyopengleschersrelativifz5.png Figura 4.1: Captura de un cielo mostrando la Luna, estrellas y una estrella fugaz. objetos de extrusión necesarios, compila la escena y luego la renderiza. Se proveen dos archivos descriptores de escena scene.dat, el cual provee la escena presentada por M. C. Escher; y el archivo externscene.dat, el cual provee el completado del escenario para hacer natural la recorida por los lı́mites del escenario. 4.1.13. stars La clase stars encapsula el manejo completo de la visualización y animación del cielo (figura 4.1). La misma se instancia a partir de un centro, un radio y la cantidad de objetos a representar. Luego, se encarga de generar la esfera que contiene a todos sus objetos, dentro de ese radio con ese centro. Los objetos que representa y anima la clase son 4 Estrellas: Las estrellas se generan y compilan al realizar la carga de la clase. Por omisión se generan 1000 estrellas, las cuales se posicionan de manera aleatoria a lo largo de una esfera del radio dado. Las estrellas se generan sin textura, con emisión blanca al 100 %. Al renderizar la escena, se actualiza el valor de la declinación del cielo en 0,005◦ , por lo que el mismo gira lentamente. La declinación del cielo ata a las transformaciones de los demás objetos, esta declinación, representa la rotación de la tierra. Planetas: Los planetas se generan aleatoriamente, con un eje de giro, un ángulo inicial y un color (con principal componente en rojo). Se compila un único planeta al generar la escena, y al dibujar se aplican las transformaciones necesarias para posicionar cada planeta y colorearlo y se dibuja al patrón compilado. En cada redibujado se actualiza el ángulo de la órbita de los planetas en 0,01◦ . Estrellas fugaces: Las estrellas fugaces funcionan de la misma manera que los planetas. La única diferencia está en que son más complejas las transformaciones que posicionan a cada instancia en el cielo, dado que, al tener una cola, las mismas son orientables y no basta simplemente con posicionarlas en su sitio. En cada iteración, las estrellas se desplazan 0,1◦ . Luna: La Luna son dos hemiesferas dibujadas como sólidos de revolución, las cuales forman un globo. Cada una de las dos mitades tiene una componente de luz difusa diferente, siendo una practicamente blanca (un poco azulada) y la otra negra; y teniendo, ambas, una mı́nima componente de luz ambiente. La esfera tiene aplicado un mapa esférico con una fotografı́a de la superficie lunar, esta vez con filtros lineales para lograr más definición. Esta conformación en dos gajos, da el efecto de dos fases. La esfera gira sobre su propio eje, por lo que la fase de la misma va cambiando en 0,1◦ por iteración; la 18 textura aplicada como mapa esférico hace que se mantenga en su sitio independientemente de la rotación de la esfera; por lo que se cumple con la apariencia de iluminación que pega desde diferentes ángulos, dando lugar a fases naturales con llenos y nuevos completos. La luna se desplaza a una ◦ velocidad de 0,012 frame , tangencial al movimiento estelar, se mueve apenas un poco más rápido que los planetas, dando sensación de cercanı́a. El radio con el que se está representando al cielo es de 100 unidades; tomando en cuenta que la escena llega hasta 30 unidades desde su centro, es muy poca la distancia del cielo a la tierra. Esto provoca que al caminar en linea recta, se avance conra las estrellas, lo cual es un efecto indeseado. No se ha puesto el cielo a más distancia porque la merma en la resolución del buffer de profundidad para objetos cercanos hacı́a estragos con el aliasing de las texturas rayadas. Cambiando los patrones de texturas del modelo es más que factible alejar el cielo sin tener efectos colaterales. 4.1.14. tp3 El módulo tp3 contiene a la clase screen, desarrollada según el paradigma de diseño fundamentado en el TP2 de esta misma materia. La misma provee la funcionalidad de alto nivel, dejandole el trabajo a las clases de bajo. Desde aquı́ se instancia la ventana y se configuran las perspectivas y puntos de vista; se registran los callbacks de teclado, y se lleva el ı́ndice de los diferentes objetos de bajo nivel que dibujan la escena. La clase screen incializa las texturas, los escenarios, las escenas, y genera las estrellas. Tiene dos modos de funcionamiento, funcionamiento por cámara esférica, la cual está implementada dentro de la clase; y funcionamiento en primera persona, la cual le delega el cálculo de las coordenadas de cámara a las clases de primera persona. Cuando funciona en el modo de cámara esférica, sólo renderiza la escena básica. En el modo de primera persona, renderiza la escena completada, y también renderiza el cielo. El refresco de la escena se hace a intervalos constantes. La rutina display, como primera acción registra un timer en 50 milisegundos3 ; como los eventos en OpenGL son bloqueantes, si la rutina de dibujado se demorara más que el intervalo de timer, el evento de timer esperarı́a a que finalizara, por lo que el sistema trabajarı́a al máximo de velocidad del procesador. El callback de timer, obviamente, dispara un refresco de la escena. Se eligió esta estrategia de refresco de pantalla y no una asociada al callback de idle, para poder tener una tasa constante de refresco, cosa fundamental para que la animación fuera consistente. 3 Realmente, no se tiene mucha noción de la precisión de dicho timer. Para evitar lagueos, se quiso implementar un sistema que tratara de asegurar una tasa constante en diferentes computadoras; pero dentro de OpenGL se descrubrió que el tiempo de renderizado de la escena era proporcional al intervalo entre renderizados sucesivos. Es decir; con los 50 milisegundos, la escena se renderiza en el orden de los 60 milisegundos; pero si se subiera el intervalo a 83 milisegundos, para garantizar una tasa de 12 frames por segundo, el tiempo de generación de la escena se alarga a más de 100 milisegundos; por lo tanto, es inclusive peor la performance. 19 http://img525.imageshack.us/img525/1795/pyopengleschersrelativivz5.png Figura 4.2: Vista en primera persona de la escena, con gravedad sobre el eje y. 4.2. Iluminación La iluminación no ha sido un punto fuerte del desarrollo de la escena. La causa de esto es que la generación de un motor genérico que pudiera renderizar escenarios levantados desde scripts externos, provocó, que el manejo de iluminación quedara externo a la descripción de los polı́gonos. En un modelizado hardcodeado en el código fuente hubiera sido trivial agregar diversas fuentes de luz según los objetos a dibujar, pero agregar esta misma funcionalidad al lenguaje descriptor hubiera complicado exponencialmente los mecanismos del motor. Por otro lado, la ausencia de cálculo de sombras en una o más pasadas en la API de OpenGL hace que, por las caracterı́sticas de los objetos, un modelo de iluminación exterior, con luces fuertes, evidencie la ausencia de luces, dando una apariencia antinatural. Es por esto que la iluminación se limitó a dos modelos de iluminación diferentes. La fuente principal de luz es el observador; quien va iluminando con su vista... esto resuelve el problema de las sombras, dado que no deberı́a ver ninguna si él provee la luz; y crea una atmósfera agradable al realzar los objetos con emisión total que se encuentran en el cielo. La otra iluminación surge de la Luna; quien ilumina de manera difusa el espacio; si bien esta es una iluminación tenue, la misma es perceptible en la escena, y da una ambientación razonable al espacio. 4.3. Funcionamiento Los controles de la aplicación se realizan desde el teclado, más que nada por una simplificación en la operación al momento de desarrollar; es probable que se evolucione hacia controles con mouse a futuro. Los modos de funcionamiento, como ya se describieron, son dos; visión esférica y primera persona (figura 4.2). En el modo de vista esférica la cámara rota lentamente describiendo un cilindro con respecto al centro de la escena. Los controles asociados al modo son las teclas j, l, i y k, las cuales giran hacia izquierda, derecha, arriba y abajo, respectivamente; controlando las dos primeras el ángulo de giro sobre al eje z y las dos últimas el ángulo con respecto al plano xy. Se accede al modo de vista en perspectiva con cámara esférica mediante la tecla p. En el modo en primera persona la cámara se ajusta al punto de vista de un observador dentro de una de las diferentes gravedades. Se selecciona uno de los tres modos en primera persona mediante las teclas x, y y z, asociada cada letra con el plano sobre el cual se manifiestan las alturas del escenario. Los controles vuelven a ser i, k, j y l, esta vez representando avance, retroceso, giro a izquierda y giro a derecha, respectvamente. Para salir de la aplicación se puede presionar la tecla q o ESC, en cualquier modo de funcionamiento. 20 5. Trabajo a futuro La aplicación fue acotada para poder se entregada dentro de los plazos de la materia. Pese a las cotas que se impusieron, su desarrollo llevó más de un mes y medio de trabajo sostenido; se estima que al trabajo le falta un mes más de dedicación para llegar a una versión pulida. La idea es publicar la aplicación, por lo que se espera poder mejorar los detalles que quedaron pendientes. Terminar la inclusión de personajes, agregar más objetos en la escena, mejorar el modelo de iluminación, pulir los modelos de objetos, etc.. Se considera, de todos modos, que la presente entrega tiene una madurez suficiente como para ser expuesta como una primera versión beta del trabajo. 21