tabla de contenido · página 1 de 134 tabla de contenido ... diagrama de colores de una niebla ......
TRANSCRIPT
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 1 de 134
Tabla de contenido 1 Introducción .......................................................................................................................... 6
1.1 Marco ............................................................................................................................ 6
1.2 Estado del arte .............................................................................................................. 7
1.3 Descripción del problema ........................................................................................... 12
1.4 Solución propuesta...................................................................................................... 14
1.5 Perspectiva general del proyecto ................................................................................ 16
2 Fundamentos teóricos ........................................................................................................ 17
2.1 Librería gráfica ............................................................................................................. 17
2.1.1 Introducción ........................................................................................................ 17
2.1.2 DirectX 9.0c ......................................................................................................... 20
2.1.3 DXUT .................................................................................................................... 22
2.2 Renderizado ................................................................................................................ 24
2.2.1 Iluminación .......................................................................................................... 24
2.2.2 Luces .................................................................................................................... 32
2.2.3 Texturas ............................................................................................................... 34
2.2.4 Materiales ........................................................................................................... 44
2.2.5 Render Targets .................................................................................................... 47
2.2.6 Efectos Especiales................................................................................................ 49
2.2.7 Estructura Pipe-Line ............................................................................................ 54
2.3 Tratamiento de eventos .............................................................................................. 55
2.3.1 Trama .................................................................................................................. 55
2.3.2 Eventos ................................................................................................................ 57
2.3.3 Entrada / Salida ................................................................................................... 58
2.4 Escena .......................................................................................................................... 59
2.4.1 Vértices flexibles ................................................................................................. 59
2.4.2 Vectores de índices y vértices ............................................................................. 60
2.4.3 Mallas .................................................................................................................. 62
2.4.4 Submallas ............................................................................................................ 65
2.4.5 Bounding Boxes ................................................................................................... 68
2.4.6 Cámaras ............................................................................................................... 69
2.4.7 Frustrums ............................................................................................................ 71
2.5 Sombras ....................................................................................................................... 73
2.5.1 Introducción ........................................................................................................ 73
2.5.2 Cube Shadow Maps ............................................................................................. 74
2.6 Exportador ................................................................................................................... 77
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 2 de 134
3 Parte práctica ...................................................................................................................... 80
3.1 Librería gráfica ............................................................................................................. 80
3.1.1 DirectX 9.0c ......................................................................................................... 80
3.1.2 DXUT .................................................................................................................... 82
3.2 Renderizado ................................................................................................................ 84
3.2.1 Iluminación .......................................................................................................... 84
3.2.2 Luces .................................................................................................................... 91
3.2.3 Texturas ............................................................................................................... 94
3.2.4 Materiales ........................................................................................................... 97
3.2.5 Render Targets .................................................................................................. 100
3.2.6 Efectos Especiales.............................................................................................. 102
3.2.7 Estructura Pipe-Line .......................................................................................... 107
3.3 Tratamiento de eventos ............................................................................................ 112
3.3.1 Trama ................................................................................................................ 112
3.3.2 Eventos .............................................................................................................. 113
3.3.3 Entrada / Salida ................................................................................................. 114
3.4 Escena ........................................................................................................................ 115
3.4.1 Vértices flexibles ............................................................................................... 115
3.4.2 Vectores de índices y vértices ........................................................................... 116
3.4.3 Mallas ................................................................................................................ 117
3.4.4 Submallas .......................................................................................................... 119
3.4.5 Bounding Boxes ................................................................................................. 120
3.4.6 Cámaras ............................................................................................................. 122
3.4.7 Frustrums .......................................................................................................... 123
3.5 Sombras ..................................................................................................................... 124
3.5.1 Cube Shadow Maps ........................................................................................... 124
3.6 Exportador ................................................................................................................. 126
4 Resultados ......................................................................................................................... 129
5 Conclusión ......................................................................................................................... 131
6 Líneas de futuro ................................................................................................................ 132
7 Glosario ............................................................................................................................. 133
8 Bibliografía ........................................................................................................................ 134
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 3 de 134
Listado de Ilustraciones Ilustración 1 - Capas según nivel ................................................................................................. 18 Ilustración 2 - Capas según nivel con DXUT añadido .................................................................. 22 Ilustración 3 - Componentes del Modelo Phong ......................................................................... 25 Ilustración 4 - Escena limpia ........................................................................................................ 26 Ilustración 5 - Cálculo de la componente Difusa ......................................................................... 27 Ilustración 6 - Cálculo de la componente Especular ................................................................... 28 Ilustración 7 - Diferentes valores de n para la componente especular ...................................... 29 Ilustración 8 - Diferentes tipos de modelos de iluminación ....................................................... 30 Ilustración 9 - Diferentes resultados según parámetros A i B ..................................................... 31 Ilustración 10 - Diagrama luz omnidireccional ............................................................................ 34 Ilustración 11 - Muestras de texturas con componente Difusa .................................................. 36 Ilustración 12 - Texturas de componente Especular con sus respectivas Difusas ...................... 37 Ilustración 13 - Interpolación trivial entre normales por vértice ................................................ 39 Ilustración 14 - Interpolación entre normales por vértice .......................................................... 39 Ilustración 15 - Interpolación mediante textura de normales .................................................... 39 Ilustración 16 - Componente emisiva aplicada a un objeto ........................................................ 41 Ilustración 17 - Efecto del Mapa de Luces .................................................................................. 42 Ilustración 18 - Diagrama del Manager de Texturas ................................................................... 43 Ilustración 19 - Estructura de un material .................................................................................. 44 Ilustración 20 - Diagrama del Manager de Materiales ................................................................ 46 Ilustración 21 - Diagrama de renderizado ................................................................................... 47 Ilustración 22 - Diagrama de renderizado con Render Targets .................................................. 48 Ilustración 23 - Diagrama de ejemplos de distancias de Nieblas ................................................ 50 Ilustración 24 - Diagrama de colores de una Niebla ................................................................... 50 Ilustración 25 - Diagrama de flujo del proceso Glow .................................................................. 51 Ilustración 26 - Proceso teórico de Glow a nivel de píxel ........................................................... 52 Ilustración 27 - Diagrama de funcionamiento del Glow mediante Kawase ................................ 53 Ilustración 28 - Diagrama del Pipe-Line utilizado para renderizar .............................................. 54 Ilustración 29 - Diagrama de Lectura / Escritura en una trama .................................................. 56 Ilustración 30 - Ejemplo de evento ............................................................................................. 58 Ilustración 31 - Diferentes formatos de vértice .......................................................................... 60 Ilustración 32 - Index y Vertex Buffer interrelacionados ............................................................ 61 Ilustración 33 - Diagrama lógico de una malla ............................................................................ 63 Ilustración 34 - Diagrama del Manager de Mallas ...................................................................... 64 Ilustración 35 - Especificación de la Submalla ............................................................................. 66 Ilustración 36 - Diagrama del manager de Submallas ................................................................. 67 Ilustración 37 - Ejemplos de Bounding Boxes ............................................................................. 68 Ilustración 38 - Diagrama de cámaras en primera y tercera persona ......................................... 70 Ilustración 39 - Volumen de un Frustrum ................................................................................... 71 Ilustración 40 - Estructura de un cubemap ................................................................................. 74 Ilustración 41 - Ejemplo de acceso a la información de un cubemap ......................................... 75 Ilustración 42 - Ejemplo de aplicación de sombras ..................................................................... 76 Ilustración 43 - Partes del software a unir .................................................................................. 77 Ilustración 44 - Diagrama de las fases del Constructor ............................................................... 79 Ilustración 45 - Diagrama matemático de la Luz ......................................................................... 92
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 4 de 134
Listado de Tablas Tabla 1 - Especificaciones Hardware ............................................................................................. 6 Tabla 2 - Especificaciones Software .............................................................................................. 6 Tabla 3 - Logos de las soluciones de pago ..................................................................................... 8 Tabla 4 - Funcionalidades de las soluciones de pago .................................................................... 9 Tabla 5 – Renders de soluciones de pago ..................................................................................... 9 Tabla 6 - Logos soluciones libres ................................................................................................. 10 Tabla 7 - Funcionalidades soluciones libres ................................................................................ 10 Tabla 8 - Renders soluciones libres ............................................................................................. 11 Tabla 9 - Versiones de DirectX .................................................................................................... 19 Tabla 10 - Fusiones / Sustituciones ............................................................................................. 21 Tabla 11 - Coeficientes de los planos del Frustrum..................................................................... 72 Tabla 12 - Fases del Constructor ................................................................................................. 78 Tabla 13 - Librerías necesarias para DirectX 9 ............................................................................ 80 Tabla 14 - Inicialización de DirectX 9.0c ...................................................................................... 80 Tabla 15 - Bucle de renderizado de la aplicación ........................................................................ 81 Tabla 16 - Librerías necesarias para DXUT .................................................................................. 82 Tabla 17 - Inicialización de los callbacks de DXUT ....................................................................... 82 Tabla 18 - Bucle de programa de DXUT ...................................................................................... 83 Tabla 19 - Retorno del código de la aplicación ........................................................................... 83 Tabla 20 - Variable de textura en el shader ................................................................................ 84 Tabla 21 - Variables de tipo lógico .............................................................................................. 84 Tabla 22 - Estructura de salida .................................................................................................... 85 Tabla 23 - Vertex Shader ............................................................................................................. 86 Tabla 24 - Cabecera y coordenadas de textura del PS ................................................................ 87 Tabla 25 - Sampleado de texturas ............................................................................................... 87 Tabla 26 - Tintado de la luz ......................................................................................................... 87 Tabla 27 - Distancias y sombreado en el PS ................................................................................ 88 Tabla 28 - Ecuación de iluminación de PS ................................................................................... 88 Tabla 29 - Cálculo de la niebla ..................................................................................................... 89 Tabla 30 - Función del PS sin subdivisiones ................................................................................ 90 Tabla 31 - Constructor de la Luz .................................................................................................. 91 Tabla 32 - Posición de la Luz ........................................................................................................ 91 Tabla 33 - Generación de los parámetros m y n de la recta ....................................................... 93 Tabla 34 - Actualización de los parámetros de la Luz ................................................................. 94 Tabla 35 - Encapsulado de Textura ............................................................................................. 95 Tabla 36 - Cuerpo de la clase textura .......................................................................................... 95 Tabla 37 - Manager de textura .................................................................................................... 96 Tabla 38 - Clase Material ............................................................................................................. 97 Tabla 39 - Formato de material ................................................................................................... 98 Tabla 40 - Clase del manager de materiales ............................................................................... 99 Tabla 41 - Clase Render Target .................................................................................................. 101 Tabla 42 - Inicialización de un Render Target ........................................................................... 102 Tabla 43 - Clase de la niebla ...................................................................................................... 103 Tabla 44 - Función de actualización de la niebla ....................................................................... 104 Tabla 45 - Código de renderizado de la niebla .......................................................................... 104 Tabla 46 - Metodología del Glow .............................................................................................. 105 Tabla 47 - Píxel Shader del post procesado Glow ..................................................................... 106 Tabla 48 - Inicialización de matrices ......................................................................................... 107 Tabla 49 - Inicialización de la geometría ................................................................................... 107
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 5 de 134
Tabla 50 - Inicialización del Glow .............................................................................................. 108 Tabla 51 - Activación del Back Buffer ........................................................................................ 108 Tabla 52 - Inicialización de los parámetros del shader ............................................................. 109 Tabla 53 - Parámetro para pintado en alambre ........................................................................ 109 Tabla 54 - Renderizado en el Back Buffer ................................................................................. 110 Tabla 55 - Renderizado de líneas .............................................................................................. 111 Tabla 56 - Finalización del proceso de render ........................................................................... 112 Tabla 57 - Clase Trama .............................................................................................................. 113 Tabla 58 - Clase evento ............................................................................................................. 114 Tabla 59 - Transformación de E/S en evento ............................................................................ 114 Tabla 60 - Vértices flexibles ....................................................................................................... 115 Tabla 61 - Definición de la memoria de un vértice flexible ....................................................... 116 Tabla 62 - Clase VertexArray ..................................................................................................... 116 Tabla 63 - Clase IndexArray ....................................................................................................... 117 Tabla 64 - Clase CSMesh ............................................................................................................ 118 Tabla 65 - Clase CSMeshMgr ..................................................................................................... 119 Tabla 66 - Clase CSSubMesh ...................................................................................................... 119 Tabla 67 - Clase CSSubMeshMgr ............................................................................................... 120 Tabla 68 - Clase CBoundingBox ................................................................................................. 121 Tabla 69 - Clase CCamera .......................................................................................................... 122 Tabla 70 - Clase CFrustrum ........................................................................................................ 123 Tabla 71 - Datos de la Clase CCubeMap .................................................................................... 125 Tabla 72 - Interfaz de la Clase CCubeMap ................................................................................. 125 Tabla 73 - Inicialización del constructor .................................................................................... 126 Tabla 74 - Vertex y Índex Buffers en el constructor .................................................................. 127 Tabla 75 - Cambio de formato de vectores ............................................................................... 127 Tabla 76 - Rotación de las normales ......................................................................................... 127 Tabla 77 - Guardando la nueva geometría ................................................................................ 128 Tabla 78 - Primera tabla de resultados ..................................................................................... 129 Tabla 79 - Segunda tabla de resultados .................................................................................... 130
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 6 de 134
1 Introducción
1.1 Marco Este proyecto ha sido realizado por Llorenç Martí García, estudiante de la universidad
Enginyeria i Arquitectura La Salle.
Asimismo se ha realizado este proyecto en el departamento de multimedia de dicha
universidad Enginyeria i Arquitectura La Salle.
Para realizar este proyecto, ha sido necesario un ordenador de elevadas prestaciones a nivel
de tecnología 3D. A continuación podemos observar detalladamente más información sobre
esta herramienta.
HARDWARE
Procesador Intel® Core™ 2 - 6600 @ 2.40 GHz
Memoria RAM 2.048 MB
Tarjeta Gráfica NVIDIA Gforce 8800 GTX - 768 MB
Tabla 1 - Especificaciones Hardware
SOFTWARE
Sistema Operativo Microsoft Windows XP® Profesional SP2
Librería Gráfica Microsoft DirectX 9.0c (1)
Kit de desarrollo de software gráfico Microsoft DirectX SDK (June 2007) (2)
Programa de diseño 3D Autodesk 3D Studio MAX 9 (32-bits)
Programa de edición fotográfica Adobe Photoshop CS
Tabla 2 - Especificaciones Software
Mi ponente ha sido Juan Andrés Fernández Munuera, Ingeniero en Informática de Sistemas y
actual profesor en la universidad Enginyeria i Arquitectura La Salle.
Agradecer enormemente la ayuda prestada por el profesor de la Universidad Pompeu Fabra,
Joan Abadia. Sin su ayuda sobre conocimientos en la librería grafica DirectX seguramente este
proyecto no gozaría de la calidad visual que ofrece actualmente.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 7 de 134
1.2 Estado del arte
Actualmente existen multitud de soluciones software para desarrollar aplicaciones 3D de
entretenimiento o visualización y representación. Estas soluciones vienen en forma de librerías
de código diseñadas para que nos permitan, de una manera más fácil y rápida, implementar
aplicaciones 3D.
A estas librerías o conjunto de código se les denomina en el mercado como Motores 3D o en
inglés Engines 3D. Estos motores nos encapsulan un código diseñado para gestionar y dirigir
los datos de nuestra aplicación liberándonos a nosotros de esa tarea.
Las funcionalidades que nos proveen estas librerías son variadas aunque siempre hay una en
común, y es la representación gráfica de los datos. Aparte de esta característica de
representación gráfica, muchas otras funcionalidades pueden estar presentes en dicho motor,
véase funcionalidades de sonido, control de físicas, control de inteligencia artificial, etc.
Estas soluciones pueden ser libres o de pago. Normalmente por naturaleza de contrato e
imagen corporativa, las soluciones de pago son más completas y ofrecen muchas más
funcionalidades que las soluciones de carácter libre o que no requieren de una inversión
económica. Hay que tener en cuenta que cuando una librería de representación 3D incluye
otras muchas funcionalidades como las antes mencionadas como físicas, inteligencia artificial,
etc. esta recibe el nombre de Motor de Juegos 3D o Game Engine 3D. Esto es así debido a la
especialización de dicha librería ante necesidades de entretenimiento, aunque también
pueden ser utilizadas para aplicaciones con muchos otros fines.
A continuación presentaremos unos ejemplos de Motores 3D y enmarcaremos algunas de sus
características pudiendo comprobar cómo las opciones de pago están dotadas de mayores
funcionalidades que las soluciones libres. Estas son Unreal Engine (3) y Cry Engine (4). Ello no
implica que no se puedan desarrollar soluciones profesionales con los dos tipos de soluciones.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 8 de 134
EJEMPLOS DE SOLUCIONES DE PAGO
EMPRESA MOTOR 3D
Tabla 3 - Logos de las soluciones de pago
Como podemos observar en la tabla anterior, aquí tenemos dos de las múltiples soluciones de
pago que existen actualmente en el mercado. Sin intentar incurrir en cuál de las soluciones es
mejor, hemos elegido estas dos soluciones porque las dos proporcionan funcionalidades
parecidas.
Estas funcionalidades son:
FUNCIONALIDADES
Render 3D Representación gráfica en pantalla utilizando la última tecnología
disponible en su día. Nos proporciona el uso de técnicas avanzadas de
pintado como de sombreado al igual como de iluminación.
Sonido 3D Capacidad de dotar a nuestras aplicaciones con sonido envolvente.
Inteligencia Artificial Capacidad de integrar en nuestras aplicaciones la habilidad de tratar
con entes pre-programados para que actúen con una conducta
establecida o respondan a ciertas situaciones de una manera
automática.
Físicas 3D Capacidad de dotar a nuestros objetos con propiedades físicas como
la gravedad o fuerzas varias al mismo tiempo que se gestionan
correctamente las colisiones entre diferentes tipos de objetos.
Editor 3D Aplicación que permite a un artista generar contenido especifico de
este motor 3D sin necesidad de tener conocimientos de
programación previos.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 9 de 134
Animaciones 3D Capacidad de dotar a personajes y objetos de animaciones y
movimientos de complejidad elevada.
Tabla 4 - Funcionalidades de las soluciones de pago
Una vez comentadas las funcionalidades más importantes de estas soluciones de pago,
exponemos cuatro imágenes a modo de expresar la potencia y rendimiento de estas
soluciones.
IMÁGENES EN TIEMPO REAL
UNREAL TECHNOLOGY CRY ENGINE
Tabla 5 – Renders de soluciones de pago
Como podemos observar en las imágenes anteriores, son unas imágenes de una gran calidad y
de un potencial muy elevado que nos da a entender que las aplicaciones que nosotros
podamos realizar con esa solución gozarán de un gran parecido con la realidad.
A continuación mostraremos dos de las múltiples soluciones libres que existen en la
actualidad. Debido a nuestra experiencia en ellas, hemos elegido a dos de las más populares
en la comunidad de software libre sin intentar incurrir cuál de ellas es mejor, estas son Ogre3D
(5) y Crystal Space (6).
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 10 de 134
EJEMPLOS DE SOLUCIONES LIBRES
EMPRESA MOTOR 3D
Tabla 6 - Logos soluciones libres
En la tabla anterior podemos observar dos soluciones libres y populares en la comunidad de
código libre. Estas dos soluciones ofrecen menos funcionalidades que las soluciones de pago.
Veamos y razonemos el porqué.
FUNCIONALIDADES
Render 3D Representación gráfica en pantalla utilizando tecnología no
anticuada. Nos proporciona el uso de técnicas avanzadas de pintado
como de sombreado al igual como de iluminación, pero se deja en
nuestras manos el hecho de definir sistemas de iluminación de
extrema complejidad como las soluciones de pago.
Tabla 7 - Funcionalidades soluciones libres
Podemos observar como estas soluciones no pueden ser llamadas en ningún caso Motores de
Juegos 3D pues la única, aunque no fuera de mérito, funcionalidad es la de ofrecer una
representación 3D en pantalla.
Asimismo podemos observar como el hecho de crear un Motor de Juegos 3D es una tarea
realmente complicada y compleja, que requiere de un equipo de trabajo bien coordinado y
experto. Es por ese motivo que supone un reto muy grande el hecho de desarrollar una librería
tan compleja como un Motor de Juegos 3D por una entidad sin recibir fondo alguno por parte
de sus clientes, debido a que su distribución es gratuita.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 11 de 134
Aunque podamos observar menos funcionalidades en estas soluciones libres, eso no implica
que puedan representar en pantalla escenas con gran calidad e incluso profesionalidad. Dicho
esto pasamos a ver unos ejemplos en tiempo real de estas soluciones.
IMÁGENES EN TIEMPO REAL
OGRE 3D CRYSTAL SPACE
Tabla 8 - Renders soluciones libres
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 12 de 134
1.3 Descripción del problema
El problema que se nos plantea inicialmente en este proyecto radica en la investigación y el
conocimiento necesarios como para poder llevar a cabo un estudio e implementación de lo
que anteriormente hemos comentado como motor 3D.
Cabe destacar que este proyecto no tiene como finalidad obtener un producto acabado de
nivel profesional como hemos visto en el punto anterior, ya que para eso es necesaria una
cantidad de recursos de la cual ahora mismo no disponemos.
Dicho esto, la investigación de este proyecto va íntimamente relacionada con la
implementación de un motor 3D pues de esta manera se obtiene una visión y
conocimientos privilegiados sobre su gestión y expansión interna.
Actualmente el mercado de los videojuegos está en alza y eso promociona que la
tecnología que está detrás de estos haya sufrido una inversión e investigación sin
precedentes, lo que nos permite ver como día a día salen nuevas técnicas e ideas que son
implementadas en estos motores 3D.
Debido a ello, aprovecharemos el hecho de implementar un motor 3D para poder
enriquecernos a nivel intelectual y agrandar nuestra experiencia portando estas nuevas
técnicas e ideas a nuestro proyecto.
Estas técnicas e ideas pueden oscilar entre cosas relativamente sencillas hasta conceptos
realmente complicados incluso de imaginar por nosotros mismos. En este proyecto
abordaremos esas técnicas e ideas que estén al alcance siempre teniendo en cuenta los
recursos necesarios para llevarlas a su fin.
Paralelamente a la comprensión e implementación de estas ideas, un aspecto igual de
importante es el hecho de la coexistencia de estas al mismo tiempo en el motor 3D.
Debemos darnos cuenta de que un motor 3D no es solo un conjunto de técnicas e ideas
que funcionan por separado sino que deben de ser integradas y funcionar codo con codo
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 13 de 134
unas con otras. Por eso mismo, es igualmente importante el hecho de implementar las
técnicas como el hecho de garantizar una gestión apropiada de ellas para que puedan
funcionar conjuntamente y darnos el resultado visual que esperamos.
Veremos que estas técnicas están basadas tanto en DirectX (1) como en C++ (7), algunas de
ellas serán propiamente de gestión y se basarán en una gestión a nivel de código C++ y
otras estarán basadas en mostrarnos un resultado visual utilizando toda la potencia gráfica
que tengamos a nuestro alcance mediante Shaders.
Este hecho nos garantiza que veremos en profundidad la implementación y gestión interna
de un motor 3D, que aunque no llegue a la complejidad de un producto profesional, sí que
será digno de mención a nivel de investigación y experimentación.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 14 de 134
1.4 Solución propuesta
Como ya hemos podido intuir en el punto anterior, la solución propuesta para el problema
redactado anteriormente es la implementación de un motor 3D.
Un motor 3D puede partir de múltiples ideas y metodologías de funcionamiento, tantas como
personas haya dispuestas a implementar un motor 3D. Esto es así debido a que cada
programador o equipo de programadores elige previamente cómo funcionará este motor 3D y
cómo se comportará según sus necesidades.
Gracias a que nosotros poseemos experiencia con motores 3D de software libre, esto nos
acerca mucho mas a una solución hábil y estructurada debido a que ciertas metodologías
utilizadas por estos motores son inculcadas en nosotros mientras programamos con ellos.
Debido a esto, poseemos experiencia con motores como Ogre3D (5), Crystal Space (6), etc. Los
cuales nos ofrecen ideas maduras de cómo se pueden gestionar ciertos recursos que
posteriormente nosotros mismos habremos de abordar.
El motor 3D que implementaremos nos proporcionará una solución gráfica completa a nivel de
render, esto quiere decir que el producto resultante será capaz de mostrar en pantalla efectos
de iluminación avanzados, sistemas de sobra, ambientación, etc. Que igualmente
comentaremos y detallaremos en profundidad más adelante en este documento.
Todo motor 3D acostumbra a ir acompañado de un editor gráfico el cual nos permite generar
geometría para este. Actualmente estos editores se ven ayudados por programas
profesionales de modelado 3D.
En nuestro caso concreto, somos conscientes como hemos comentado anteriormente de que
tenemos recursos limitados como puede ser el tiempo. Debido a eso, optamos por crear un
exportador de geometría compleja desde el programa de modelado 3D Studio Max (8).
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 15 de 134
Este exportador nos ayudará en el cometido de generación de geometría, siendo menos
complejo que un editor profesional pero mostrando igualmente una complejidad elevada en
su implementación.
Debido a su complejidad, este exportador será considerado como parte de la solución al igual
como el motor 3D a implementar y será documentado extensamente más adelante.
En ningún momento se pretende dotar a este motor con capacidad física. Esto es así debido a
que nos queremos centrar en la implementación y gestión de las funcionalidades gráficas y las
soluciones físicas son demasiado completas tratándose de soluciones profesionales, las cuales
ya están preparadas para ser integradas en motores 3D, dejándonos solo el trabajo de
gestionar dichas físicas impidiéndonos investigar en profundidad sobre ellas.
A nivel de programación, el motor 3D será programado bajo la librería DirectX 9.0c (1) debido
a que nuestra herramienta de trabajo detallada anteriormente trabaja con el sistema
operativo Windows XP Professional y no es posible tener acceso a la librería DirectX 10 en este
sistema operativo. Para eso sería necesario cambiar de sistema operativo a Windows Vista y es
un camino que se descartó en un principio.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 16 de 134
1.5 Perspectiva general del proyecto
Hasta aquí, lo que hemos podido ver de este escrito han sido los apartados introductorios,
tanto las listas de páginas, tablas e ilustraciones como los requisitos del mismo proyecto.
Una vez llegados a este punto, empezaremos por la parte teórica del proyecto. La parte teórica
nos ocupará gran parte de dicho escrito debido a que se explican todos los conceptos con sus
diagramas correspondientes para poder llevar a cabo el trabajo que tenemos en mente.
Finalizada la sección teórica nos adentraremos en la parte práctica, que será un camino
parecido al apartado teórico con la diferencia de que en esta sección se explicaran los detalles
técnicos de los conceptos teóricos expuestos previamente.
En el apartado de resultados y gracias a que la naturaleza de nuestro proyecto ofrece
resultados gráficos, mostraremos imágenes sacadas en tiempo real de la demostración de
nuestro proyecto.
Seguiremos con las conclusiones que hemos obtenido de la realización de nuestro proyecto
seguido de las líneas de futuro, es decir, de cómo encararíamos la continuación del proyecto
una vez terminado el actual.
Finalmente nos encontraremos con un glosario de siglas técnicas y la bibliografía consultada
para nuestro proyecto.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 17 de 134
2 Fundamentos teóricos
2.1 Librería gráfica
2.1.1 Introducción
Antes de que saliese el famoso sistema operativo Windows 95, había muchos juegos que se
ejecutaban bajo MS-DOS. Estos videojuegos tenían que ser programados por sus
desarrolladores de tal forma que ellos mismos eran los encargados de la detección y acceso a
los sistemas hardware que tenía el ordenador a través del sistema operativo.
Ese hardware que debían detectar era lo que hoy conocemos como tarjetas gráficas, tarjetas
de sonido y periféricos de entrada como el teclado, ratón y joystick.
En esa época se utilizaba el estándar de la asociación VESA (9) para las tarjetas gráficas y como
estándar de sonido se utilizaban las librerías de Sound Blaster de Creative (10).
A medida que pasaron los años, los productos relacionados con los videojuegos, ya sean
tarjetas gráficas, teclados, ratones, etc. Se vieron multiplicados en número dando un gran
abanico de posibilidades en el mercado.
Los programadores debían programar la detección y acceso a un número cada vez mayor de
productos existentes, esto derivaba en plazos de desarrollo más largos y niveles de
complejidad más elevados, los cuales podían provocar más errores en la programación de la
aplicación en curso.
En los sistemas operativos Windows, el acceso directo al hardware estaba protegido por éste,
debido a ello, el acceso directo a tarjetas gráficas y tarjetas de sonido era lento e ineficaz.
Como solución a este problema surgió la librería DirectX para Windows 95.
DirectX es una librería que está por encima del hardware y que se utiliza comúnmente para el
desarrollo de aplicaciones gráficas como por ejemplo videojuegos. El sistema operativo
permite a la librería DirectX el acceso al hardware de una manera directa y eficiente.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 18 de 134
De esta manera, los desarrolladores de una aplicación gráfica como por ejemplo un videojuego
solo programan en relación a la librería DirectX, y es la propia DirectX la que se encarga de
enviar y gestionar la información a los drivers del hardware correspondiente de una forma
rápida y eficaz.
Ilustración 1 - Capas según nivel
Tres programadores llamados Alex St. John, Eisler y Eric Engstrom se plantearon convertir a
Windows 95 en una plataforma en la que los juegos pudiesen acceder a las altas prestaciones
de las tarjetas gráficas. Los tres desarrolladores crearon en noviembre de 1994 un SDK para el
desarrollo de juegos
A continuación se muestra una tabla resumen de todas las versiones que ha sufrido DirectX a
lo largo de los años. La tabla está dividida en 3 bloques, el nombre de la versión, el sistema
operativo objetivo, y la fecha de salida.
VERSIÓN SISTEMA OPERATIVO FECHA LANZAMINENTO
DirectX 1.0 30 septiembre 1995
DirectX 2.0/2.0a Windows 95 OSR2 y NT 4.0 5 junio 1996
DirectX 3.0/3.0a Windows NT 4.0 SP3
Última versión para Windows NT 4.0
15 septiembre 1996
DirectX 4.0 Nunca se lanzó
DirectX 5.0 Disponible como beta para Windows NT
5.0 que se instala en Windows NT 4.0
16 julio 1997
DirectX 5.1 1 diciembre 1997
Aplicación 1 Aplicación 2
Aplicación 3
DirectX
Sistema Operativo
Hardware
Alto nivel
Bajo nivel
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 19 de 134
DirectX 5.2 DirectX 5.2 para Windows 95 5 mayo 1998
DirectX 5.2 Windows 98 5 mayo 1998
DirectX 6.0 Windows CE para la consola Dreamcast 7 agosto 1998
DirectX 6.1 Windows 98 SE
Última versión para Windows NT 4.0
3 febrero 1999
DirectX 7.0 Windows 2000 22 septiembre 1999
DirectX 7.0a 1999
DirectX 7.1 Windows ME 16 septiembre 1999
DirectX 8.0 (RC0) 30 septiembre 2000
DirectX 8.0 (RC14) Xbox 3 noviembre 2000
DirectX 8.0a (RC14) Última versión para Windows 95 7 noviembre 2000
DirectX 8.1 (RC7) Windows XP 12 noviembre 2001
DirectX 9.0 Windows Server 2003 19 diciembre 2002
DirectX 9.0a 26 marzo 2003
DirectX 9.0b (RC2) 13 agosto 2003
DirectX 9.0c (RC0) Windows XP SP2, Windows Server 2003
SP1, y Xbox 360.
13 diciembre 2004
DirectX 9.0c Compatible con todas las versiones de
Windows en las que DirectX 9.0C (RC0)
era compatible
Primera inclusión de las DLL’s D3DX
9 diciembre 2005
DirectX 9.0c Las versiones de diciembre de 2005 y
febrero de 2006 añaden el formato XML a
algunas clases.
Agosto 2005, con
actualizaciones
bimensuales
DirectX 10.0 Exclusivo para Windows Vista 30 noviembre 2006
Tabla 9 - Versiones de DirectX
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 20 de 134
2.1.2 DirectX 9.0c
Como hemos podido ver en la descripción de la solución al problema planteado, nuestro
desarrollo se hará encima de la librería DirectX 9.0c, ya que la librería DirectX 10 es exclusiva
para Windows Vista.
Al existir en el mercado diferentes tipos de soluciones hardware a nivel tanto de periféricos
como de tarjetas integradas, ya sean gráficas o de sonido, se desarrollaron diferentes tipos de
API’s para tratarlos. Todas en su conjunto forman lo que conocemos como librería DirectX.
A continuación podemos observar una lista de las mencionadas API’s junto a una descripción
de su uso.
DirectX Graphics: Gráficos.
o DirectDraw: Usada para gráficos 2D, aunque ya en desuso.
o Direct3D: Usada para gráficos 3D.
Direct Input: Usada para capturar entrada / salida de datos (teclados, ratones, etc.)
Direct Play: Usada para la comunicación en red, aunque en desuso.
Direct Sound: Usada para la reproducción y grabación de Sonido.
o Direct3DSound: Sonido 3D.
Direct Music: Usada para la reproducción de sonidos y música en tiempo real.
DirectX Media: Usada para diferentes tipos de objetos multimedia.
o DirectAnimation: API para animación.
o DirectShow: API para tratamiento de video.
o DirectX Video Acceleration: API para aceleración de video.
o DirectX Retained Mode: API para el tratamiento de imágenes.
o DirectX Transform for Animation: API para el tratamiento de animaciones.
DirectX Media Objects: Usada para streaming como codificadores, decodificadores,
etc
Varias de estas API’s listadas anteriormente están en desuso debido a que la empresa
Microsoft está introduciendo muchos cambios en la última librería de DirectX. Estos cambios
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 21 de 134
son debidos a una fusión / sustitución de ciertas API’s en favor de otras para poder tener una
plataforma unificada con la consola Xbox 360 propiedad de Microsoft.
Un ejemplo de estas fusiones / sustituciones las podemos observar en la siguiente tabla:
ANTIGUO NUEVO
Direct Input XInput
Direct Play Xbox Live
Direct Show MediaFundation
Direct Sound XACT
Tabla 10 - Fusiones / Sustituciones
Actualmente las API’s que están más relacionadas con el mundo de los videojuegos son:
Direct3D: Usada para la representación en 3D de geometría mediante aceleración
hardware.
Direct Input: Usada para la gestión de entrada / salida de datos por parte del usuario
mediante periféricos determinados como teclados, ratones, etc.
Direct3DSound: Usada para la reproducción de sonido 3D en videojuegos.
Direct Show: Usada para la reproducción de videos aunque con la entrada de las
nuevas API’s ésta se está viendo relegada a un segundo plano
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 22 de 134
2.1.3 DXUT
DXUT proviene de la agrupación de las siglas en ingles DirectX Utility Toolkit (2), que traducido
al castellano es una librería de utilidades para DirectX.
Esta librería fue creada encima de las API’s de DirectX 9.0 y DirectX 10 para ayudar al
programador a crear utilidades gráficas, juegos y otras aplicaciones de una forma más robusta
y con mayor facilidad de implementación.
La capa que supone DXUT (2) está diseñada para ayudar a los programadores a invertir menor
tiempo en la programación y corrección de errores en aspectos redundantes y repetitivos que
podemos encontrar en una aplicación gráfica.
A continuación observaremos una ilustración donde veremos claramente dónde está situada
esta capa y cuál es su nivel de acceso por parte de las aplicaciones.
Ilustración 2 - Capas según nivel con DXUT añadido
En la ilustración anterior podemos observar como la inclusión de la librería DXUT en nuestra
aplicación es totalmente dependiente de nuestra decisión. Podemos encontrar aplicaciones
que hagan uso de ella, como también podemos encontrar aplicaciones que no deseen
utilizarla.
DXUT nos proporciona ayuda en algunas de las tareas repetitivas que comentábamos antes.
Un ejemplo de ellas lo podemos ver a continuación:
Aplicación 1 Aplicación 2
Aplicación 3
DirectX
Sistema Operativo
Hardware
Alto nivel
Bajo nivel
DXUT
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 23 de 134
Creación de una ventana
Elegir el Device de DirectX
Crear el Device de DirectX
Manejar los eventos del Device
Manejar los eventos de la ventana
Cambios entre aplicación de ventana y aplicación a pantalla completa
Cuando hablamos de Device hemos de referirnos a ese objeto genérico al cual trataremos
mediante paso de información y parámetros para conseguir resultados gráficos.
En nuestro caso, DirectX 9.0 nos proporciona un Device por cada una de las tarjetas gráficas
que tengamos en nuestro equipo. Nosotros elegimos con qué tarjeta o Device queremos
trabajar y a partir de ese momento interactuaremos con ella / el mediante parámetros para
obtener los resultados gráficos esperados.
DXUT funciona encima de las API’s de DirectX 9.0 y DirectX 10, debido a eso, una aplicación
que haga uso de de esta librería de utilidades puede hacer un uso de forma sencilla de estas
API’s. Gracias a las utilidades de DXUT, nuestra aplicación puede detectar que API tenemos
instalada en nuestro ordenador, si éste contiene la librería DirectX 10 y puede ejecutar las dos
anteriormente mencionadas, por defecto DXUT utilizará DirectX 10 para realizar todo el
trabajo.
Al mismo tiempo, DXUT nos proporciona acceso a una serie de controles mediante los cuales
podemos crear de forma sencilla una GUI para nuestra aplicación. Esto es de gran ayuda ya
que de lo contrario, el programador tendría que destinar mucho tiempo a la implementación
de una interface gráfica que permitiese al usuario interactuar con dicha aplicación.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 24 de 134
2.2 Renderizado
2.2.1 Iluminación
2.2.1.1 Introducción
En el motor 3D que estamos implementando, un aspecto fundamental de éste es la
iluminación. La iluminación la entenderemos a partir de ahora como esa técnica o conjunto de
técnicas las cuales nos permitirán dotar a una escena con propiedades lumínicas las cuales, a
su vez, nos ayudarán a interpretar mejor la información visual que estamos viendo.
El impacto de la iluminación en una escena siempre viene acompañado por los efectos de
sombreado. Esta dualidad de luz – oscuridad permite al ojo humano interpretar de manera
notablemente superior la información geométrica que se nos muestra en pantalla.
Este efecto lo podemos describir como percepción de la profundidad de la escena. No
debemos olvidar de que toda aplicación gráfica 3D tiene como resultado final una
representación mediante píxeles en una imagen plana, en nuestro caso esa imagen plana será
lo que nos muestra el monitor.
Para crear la iluminación a nivel matemático, es necesario utilizar una ecuación matemática
que nos ayude en el cálculo. De estas ecuaciones hay muchas y de complejidad variable. En
medios de información como internet al igual como con libros, podemos encontrar que no se
habla de ecuaciones como tal sino de modelos.
Nosotros en nuestro motor hemos implementado el modelo de iluminación Phong (11),
aunque también hablaremos de otro modelo más complejo llamado Oren – Nayar (12).
La iluminación que implementaremos en nuestro motor 3D estará programada mediante
Shaders, una técnica puesta en escena para permitir una mayor libertad a los programadores y
diseñadores en su labor para crear aplicaciones gráficas.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 25 de 134
2.2.1.2 Principios teóricos de la iluminación
El modelo de Phong estima la iluminación en un punto a partir de la posición de la luz, de la
normal asociada a ese punto de la superficie y de las características de esta superficie.
Inicialmente en el sistema nosotros sabremos en todo momento la posición de nuestra luz, la
normal en el punto donde vamos a hacer los cálculos y las características de ese punto. Estas
características pueden ser muchas y muy variadas. Por ahora solo nos centraremos en la
cantidad de componente especular que posee dicho punto.
A continuación podemos ver una ilustración la cual nos muestra las componentes que tiene en
cuenta el modelo de iluminación Phong:
Ilustración 3 - Componentes del Modelo Phong
Podemos observar en la ilustración anterior 3 componentes que sumadas nos dan el resultado
final. Estas componentes son Ambiente, Difusa y Especular.
Ambiente:
o Esta componente normalmente se refiere a un color homogéneo para toda la
superficie que queremos pintar. Debemos pensar en ella como ese color que
proporciona la escena a esa superficie. En muchos casos esta componente es
representada como un valor nulo o cero debido a que se deja a la componente
Difusa todo el trabajo de la representación de color.
Difusa:
o Esta componente es la poseedora de toda la información de color base que
posee nuestra superficie. Veremos posteriormente como a la hora de calcular
Ambiente Difusa Especular Resultado + + =
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 26 de 134
esta componente, se generan áreas donde el color es más original y áreas
donde el color es prácticamente negro, cosa que indica ausencia de luz debido
a la posición de ese punto con respecto a la luz que ilumina.
Especular:
o La componente especular nos da información visual de lo brillante que resulta
nuestra superficie en reacción a nuestra luz. Podemos oscilar entre la
superficie mas mate de todas, la cual tendría una componente especular de
cero, hasta la superficie más brillante de todas, que tendría una componente
especular de uno.
Vista una definición de estas tres componentes las cuales forman el modelo de Phong, a
continuación mostraremos como se realizan los cálculos necesarios para poder llegar al
resultado final que nos plantea este modelo.
Primero de todo definiremos una escena donde tendremos un píxel a iluminar, tendremos
también un emisor de luz, y un receptor de ella. Este receptor normalmente es un ojo humano
o en nuestro caso una cámara, la cámara desde donde nosotros vemos la escena.
Ilustración 4 - Escena limpia
Podemos observar en la escena anterior como disponemos de un emisor de luz, representado
por el sol de color naranja. Un receptor que pasaremos a ser nosotros imaginariamente
representado por una cara, y el componente más importante de la escena, el píxel a iluminar
que será representado por el cuadrado verde.
Este cuadrado verde representa uno de los múltiples píxeles que forman la superficie
representada por el rectángulo de color blanco. Hay que tener en cuenta de que vamos a
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 27 de 134
tratar siempre estos ejemplos como si solo estuviéramos hablando de un píxel. En la realidad,
todos los cálculos que vamos a describir en estas escenas, son calculados para todos los píxeles
que forman la superficie.
El cálculo de la componente ambiente es el más sencillo de realizar debido a que éste es
simplemente una constante que hace referencia al color que recibe el objeto actual del
ambiente. En muchos casos nos encontraremos con que esta constante es cero.
Una vez tenemos la componente ambiente del modelo de Phong, pasamos a calcular la
componente Difusa. Esta componente ya no es tan trivial como la anterior. Concretamente
esta componente se define como la intensidad que ofrece la luz emisora multiplicado por el
coseno del ángulo que forma el vector incidente que va desde la luz al píxel con la normal de
dicho píxel.
Ilustración 5 - Cálculo de la componente Difusa
Como podemos observar en la fórmula anterior, el coseno del ángulo entre el vector L y el
vector N puede ser calculado igualmente mediante el productor escalar entre estos dos
vectores. Es importante destacar el hecho de que el vector L ha de ser invertido para que el
cálculo tenga efecto tal y como se requiere, debido a esto, en la formula siguiente el vector L
aparece en negativo para que el cálculo sea correcto.
L N
Ѳ
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 28 de 134
Una vez calculada la componente Difusa del modelo de Phong, nos disponemos a calcular la
componente Especular, que será la encargada de darnos información sobre el brillo del
modelo en la escena. Para esto, es necesario calcular dos vectores más de los que ya teníamos
anteriormente.
Primero de todo necesitamos el vector V. Este vector es el vector que va desde el píxel en
cuestión hasta el receptor, que seremos nosotros o una cámara para tal efecto.
Seguidamente necesitaremos el vector R. Este vector es el vector reflexión del vector L, es
decir, si mirásemos al vector N como una línea que actúa de espejo, el vector R es la reflexión
del vector L en ese supuesto espejo.
Por lo tanto, teniendo los vectores V y R calculados, ya podemos calcular el ángulo que hay
entre ellos dos y así poderlo aplicar a la fórmula. Seguidamente veremos una ilustración sobre
lo que hemos expuesto:
Ilustración 6 - Cálculo de la componente Especular
Partiendo de la ilustración anterior y de la fórmula que acabamos de mostrar, podemos
observar como la componente especular es calculada a partir de la intensidad de la luz y una
constante especular multiplicadas por el coseno del ángulo que forman los vectores V y R.
Hemos de destacar que el resultado del coseno del ángulo, está elevado a la enésima potencia,
esto es así ya que de esta manera se proporciona la intensidad deseada al efecto de la
componente especular.
L N
Ѳ R
V
σ
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 29 de 134
Fijémonos en que hemos cambiado el coseno del ángulo por el producto escalar entre V y R,
pues así es como el cálculo es más eficiente.
Cuando nos referimos a la variable n que está elevando al cálculo entre vectores V y R, siempre
nos hemos referido a que esta variable proporciona un valor de intensidad a la componente
especular. A continuación mostraremos un resultado visual de dicha variable para que se
puedan apreciar los cambios según los valores de n.
Ilustración 7 - Diferentes valores de n para la componente especular
Una vez calculada la componente especular, solo nos queda substituir los resultados en la
fórmula del modelo de Phong y obtener el resultado, este resultado nos vendrá dado como
color, este color será el que nosotros asignaremos al píxel representado por el cuadrado verde
que hemos estado viendo en todas las ilustraciones anteriores.
Al principio de este punto, hemos hablado sobre otro modelo, mucho más costoso de realizar
que se llama Modelo Oren – Nayar. Este modelo parte del modelo Phong pero añade unos
cálculos basados en el principio de que las superficies reales no son completamente planas.
El modelo Oren – Nayar parte de la base de que toda superficie rugosa puede ser representada
por infinidad de micro caras con diferentes orientaciones. Estas orientaciones son las que
proporcionan en un resultado final, la sensación de mayor realismo.
Este modelo parte del modelo Phong, añadiendo unos cálculos que son los que, aún siendo
teóricamente simples, a nivel de computación resultan muy costosos. Veamos un ejemplo
comparativo entre Phong y Oren – Nayar:
n = 1 n = 60
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 30 de 134
Ilustración 8 - Diferentes tipos de modelos de iluminación
Como podemos comprobar en la ilustración anterior, el modelo Oren – Nayar se acerca más al
resultado visual del objeto real, pero como contrapartida tiene mayor coste computacional
que el modelo Phong.
En la fórmula anterior, podemos observar como la fórmula derivada del modelo de Phong es
más complicada. Necesitamos calcular los parámetros A y B al igual como el cálculo de un seno
y una tangente que son los que realmente añaden complejidad computacional al modelo.
Podemos fijarnos de que si hacemos A = 1, y B = 0, entonces simplificando la ecuación
obtenemos la siguiente fórmula:
Que es precisamente el modelo de Phong. Consecuentemente, entenderemos que con valores
predeterminados como los que hemos hablado, es posible pasar del modelo Oren – Nayar al
modelo Phong sin necesidad de cambiar la ecuación.
Modelo Real Modelo Phong Modelo Oren - Nayar
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 31 de 134
A continuación podremos observar cómo puede variar el resultado visual del modelo Oren –
Nayar cambiando el parámetro σ de las componentes A i B.
Ilustración 9 - Diferentes resultados según parámetros A i B
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 32 de 134
2.2.2 Luces
2.2.2.1 Introducción Como ya hemos comentado, las luces en términos de representación 3D son emisores de luz.
Estos emisores de luz son representados mediante puntos matemáticos en el espacio. De esta
manera y dependiendo siempre de cómo se traten estos puntos, podremos tener luces en
nuestra escena.
A continuación presentamos una lista con una breve descripción de los tipos de luces que
normalmente se emplean en aplicaciones 3D. Puede haber más pues el número no está
restringido, pero con estas que se presentan a continuación se pueden hacer la gran mayoría
de las iluminaciones de una escena.
Omnidireccional
o Las luces omnidireccionales son aquellas que representadas por un punto
matemático en el espacio emiten luz en todas direcciones. El cálculo de la luz
entonces será llevado a cabo desde el centro de la luz hasta las superficies de
los objetos a iluminar. Un símil sencillo para este tipo de luces seria el astro
Sol, este astro emite luz en todas direcciones desde su centro. Y los objetos
son iluminados como tal.
Foco
o Las luces de tipo Foco son aquellas que desde su origen emiten luz en todas
direcciones pero con una restricción de ángulo. Esta restricción es la causante
de crear un efecto de iluminación en forma de cono, estando la punta de este
cono en el centro emisor de luz, y la parte más ancha de este cono en el punto
más alejado del centro de la luz. Un símil para este tipo de luz seria una
linterna, las linternas iluminan en un cono desde su origen.
Direccional
o Las luces direccionales son una aproximación matemática que se realiza para
representar luces que están a una distancia considerable del objeto a iluminar.
Esto provoca que donde antes teníamos luz emitida desde un punto y,
habiendo restricción o no de esa luz por parte del ángulo, todos los rayos
emitidos desde el origen eran radiales. En cambio, con esta aproximación
tenemos que los rayos de las luces direccionales son paralelos. Un símil para
entender estas luces seria una luz colocada muy lejos de nosotros pero que en
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 33 de 134
vez de emitir sus rayos de luz desde un punto matemático, lo hace desde un
plano perpendicular a nosotros. De esta manera, con estas luces no existen
rayos radiales, sino paralelos entre ellos.
2.2.2.2 Luces Omnidireccionales En el diseño e implementación de nuestro motor, hemos elegido este tipo de luces como la
más adecuada para poder tener tanto iluminación local como una correcta sensación de
sombreado en la escena.
Como hemos visto en la introducción, existen tres tipos de luces básicas para un motor 3D. De
estas tres, nosotros solo hemos implementado las luces Omnidireccionales. Esto es así debido
a que las luces de tipo Foco son simplemente luces Omnidireccionales a las que se les ha
introducido una restricción, y las luces direccionales serán suplidas por otro tipo de
iluminación que veremos más adelante.
Las luces omnidireccionales de nuestro motor disponen de los parámetros necesarios como
para poder cambiar su color y su atenuación.
El hecho de cambiar de color una luz es una cosa realmente trivial, pues simplemente
indicaremos que el color de la luz sea uno a nuestra elección.
Más complicación viene cuando hablamos de atenuación. La atenuación de una luz viene de
las propiedades físicas de las luces que indican que a mayor distancia, la intensidad de ésta es
menor sobre los objetos. Es necesario comentar que no es la finalidad de este proyecto
representar fielmente los efectos de la luz, pues estos podrían llevar más cálculos de los
necesarios para una aplicación en tiempo real.
La atenuación de nuestra luz omnidireccional vendrá dada por dos parámetros, estos
parámetros indicaran la distancia desde el origen hasta donde empieza la atenuación y la
distancia hasta que la atenuación es total, es decir, donde ya no llega la luz.
En la ilustración siguiente veremos un diagrama gráfico de cómo se comportan estos
parámetros dada una luz omnidireccional cualquiera.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 34 de 134
Ilustración 10 - Diagrama luz omnidireccional
En la ilustración anterior podemos observar de manera clara y concisa cuales son los dos
parámetros de la luz (flecha verde y flecha roja) que conforman los dos radios de la luz.
Asimismo podemos observar la extensión del diagrama a una representación en dos
dimensiones sobre cómo se comporta la luz a media que ésta se aleja de su centro emisor.
Podemos observar como desde el origen hasta el primero radio, la luz es máxima en todo su
recorrido, y como desde el primer radio hasta el segundo, la luz pasa de intensidad máxima a
intensidad mínima linealmente.
2.2.3 Texturas
2.2.3.1 Introducción
Un textura se define como un mapa de bits con información de color aplicada a una geometría
en una escena 2D / 3D. Por lo tanto, entenderemos como textura a partir de ahora ese fichero
Luz Omni
Radio exterior
Radio interior
Pendiente de atenuación
Área máxima iluminación
Área atenuada
Y+
X+
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 35 de 134
especial que nos contiene un mapa de colores específicos que nosotros aplicaremos de la
forma que mejor nos convenga en los modelos que utilizaremos en nuestro motor 3D.
Es importante introducir al lector de que esta información de color, no tiene porqué ser
interpretada siempre como un color, puede ser interpretada esa información como valores
numéricos para ciertos cálculos que veremos con posterioridad.
Una vez comentado que es una textura como tal y si recordamos las variables en el modelo de
Phong, no es de extrañar que en nuestro motor 3D utilicemos como fuente para esas variables
texturas destinadas a esa función.
Si recordamos el modelo de Phong, este nos decía que:
En el modelo que nosotros usaremos, cuando hablemos de Difusa, tendremos una textura que
será la encargada de proporcionarnos por cada píxel la información numérica o de color
relacionada con la componente Difusa. Lo mismo pasará con otros tipos de texturas.
En nuestro modelo de iluminación, tendremos cinco variables. A continuación mostramos una
lista con una pequeña introducción sobre cada una de ellas:
Componente Difusa
o La textura que nos dará la componente Difusa es una textura de color de 24
bits repartidos entre 3 grupos de 8 bits de Rojo, Verde y Azul más un cuarto
grupo de 8 bits para la componente transparente. Hemos de imaginarnos esta
textura como una fotografía con color real para colorear nuestros modelos en
escena.
Componente Especular
o La textura que nos proporciona la componente Especular es una textura como
la anterior, pero con la diferencia de que el color que nos muestra ésta es un
valor número entre cero (0) y uno (1) que nos indicará la cantidad de
componente Especular que se puede aplicar en ese punto en concreto.
Componente Normal
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 36 de 134
o La textura de componente Normal es una textura que no nos proporciona un
color sino un valor. Ese valor será interpretado como si de un vector se tratase
y así poder extraer una normal en el punto solicitado.
Componente Emisiva
o La textura de componente Emisiva es esa textura que nos dará información
sobre en qué puntos del modelo a tratar se está emitiendo luz desde su
superficie. Más adelante se especificará mejor la información relativa a esta
textura.
Componente Mapa de Luces
o La textura de Mapa de Luces es una textura que contiene información en
forma de color sobre la iluminación estática ambiente y que será aplicada al
objeto en cuestión.
2.2.3.2 Componente Difusa
Como ya hemos comentado, la componente Difusa es posiblemente la más sencilla de
entender, pues la información nos llegará directamente de su información de color.
La componente Difusa es la encargada de darle color a los objetos que queremos tener en
nuestra escena 3D. Por ello, el color base que tengan nuestros objetos vendrán dados de base
por esta texturas.
Ilustración 11 - Muestras de texturas con componente Difusa
Como podemos observar en la ilustración anterior, mostramos tres texturas al azar utilizadas
en nuestro motor para colorear los objetos requeridos en la escena.
Concrete1_d.tga Stones1_d.tga Lava1_d.tga
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 37 de 134
Es importante destacar también que el formato de estas texturas en concreto es de 24 bits por
píxel, 8 para el canal Rojo, 8 para el canal Verde, y 8 para el canal Azul. No tienen canal Alpha
para poder hacer transparencias porque no es requerido en esta fase del motor 3D.
Fijémonos también en el formato del nombre. El nombre de toda textura Difusa contiene una
cola característica e igual para todas ellas. Esta cola son los caracteres ‘_d’, estos caracteres
determinan de forma rápida el que esa textura en concreto sea de tipo Difusa.
2.2.3.3 Componente Especular
La componente Especular nos vendrá dada por un tipo de textura igual al de la componente
Difusa, es decir, la misma estructura en tipo de bits para el color, 24 bits divididos entre tres
pares de 8 bits para los tres canales de color.
El significado de la componente Especular es el de dejar o no dejar brillar la luz dependiendo
de la componente Especular en la superficie del objeto que estemos tratando. Para ello
primero veremos un conjunto de texturas de componente Difusa con sus respectivas texturas
de componente Especular para que podamos tener en mente su utilidad en el futuro.
Ilustración 12 - Texturas de componente Especular con sus respectivas Difusas
Panel1_d.tga
Panel2_d.tga
Panel1_s.tga
Pane2_s.tga
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 38 de 134
En la ilustración anterior podemos ver las texturas de componente Especular como van junto a
sus homónimas en cuanto a componente Difusa se refiere. Fijémonos en que solo se podrá
representar brillo allí donde la componente Especular sea más clara y no se podrá representar
brillo allí donde sea más oscura.
Este aspecto nos proporciona un cierto control sobre qué áreas pueden brillar y qué áreas no
en un objeto, aparte del poder decir si hay brillo o no, con la tonalidad de la componente
Especular. También podemos controlar la cantidad de brillo, dando mucha más libertad al
diseñador para poder mostrar objetos más naturales en escena.
2.2.3.4 Componente Normal
La componente Normal nos viene dada por una textura de 24 bits en la cual no codificaremos
color sino valores numéricos que se van a interpretar como componentes de un vector.
Cada píxel de la textura que representa la componente Normal se interpretará como un vector
que indicará la dirección de la Normal que se utilizará para hacer los cálculos de la iluminación.
Esto es así debido a que si se define una normal por vértice en los triángulos de un modelo,
cuando este modelo es rasterizado y convertido a imagen compuesta por píxeles, esta Normal
es interpolada de vértice a vértice. Esto nos ofrece una interactuación muy vaga a la hora de
calcular la iluminación en esos píxeles.
Debido a ese límite en la iluminación, se optó por dar una normal píxel a píxel en la superficie
de un modelo para así dotar a dicho modelo de una iluminación mucho más precisa y con
resultados ampliamente superiores en cuanto a calidad visual.
A continuación mostraremos una ilustración que comparará el hecho de dar normales a los
vértices y dejar que éstas se interpolen o proporcionar una textura para que ésta dé las
normales píxel a píxel y podremos comprobar cómo el control otorgado por esta última
técnica es mucho más potente.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 39 de 134
Ilustración 13 - Interpolación trivial entre normales por vértice
En la ilustración anterior podemos observar una interpolación trivial entre las normales de dos
vértices, si entendemos los dos vértices como los dos cuadrados lilas con sus respectivas
normales lilas. Entonces veremos cómo una vez rasterizado el triángulo y convertido en
píxeles, estos píxeles interpolados reciben a su vez una interpolación entre las dos normales
anteriores, quedando en este ejemplo una interpolación trivial debido a que todas ellas son la
misma.
Ilustración 14 - Interpolación entre normales por vértice
En la ilustración anterior, vemos un ejemplo de interpolación que ya no es trivial. Básicamente
es un ejemplo para mostrar cómo se comporta el sistema cuando las dos normales no son la
misma normal.
Igualmente podemos observar como los píxeles resultantes obtienen una normal interpolada
entre las dos normales que provienen de los dos vértices.
Ilustración 15 - Interpolación mediante textura de normales
Normal1_n.tga
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 40 de 134
En la ilustración anterior, la interpolación ya no viene dada por las normales entre vértices,
sino que es rescatada píxel a píxel de la textura de normales. Esto nos da un control a nivel de
píxel de cómo queremos las normales en cada momento. Mediante esta técnica es posible
dotar de gran realismo a la escena sin comprometer el rendimiento de ésta por la necesidad
de añadir más triángulos a los objetos que vemos en dicha escena.
2.2.3.5 Componente Emisiva
La componente emisiva la debemos entender como si saliese luz de la propia superficie del
objeto al que le estamos aplicando la textura emisiva.
Recordemos que las luces dinámicas que hemos implementado son puntos matemáticos en el
espacio que emite rayos de luz. Con estas luces nos es imposible representar posibles
emisiones de luz a lo largo de superficies.
Con esta información que tenemos ya podemos deducir dos cosas. La primera es que al
tratarse de una emisión que no forma parte de las luces dinámicas, este tipo de iluminación no
es dinámico y por lo tanto solo servirá para dotar a la escena de un efecto parecido a algo que
podamos ver en la realidad. La segunda es que esta iluminación solo la veremos en la fuente,
es decir, en esa superficie en la cual apliquemos la textura emisiva, no se representará ni se
proyectará encima de demás superficies cercanas.
Para continuar con la explicación, mostraremos una ilustración donde veremos este tipo de
componente aplicada a un objeto y cómo parece que emita luz siendo un efecto visual y no
una emisión de luz dinámica.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 41 de 134
Ilustración 16 - Componente emisiva aplicada a un objeto
Como podemos ver en la ilustración anterior, el efecto emisivo está marcado en el objeto
mediante flechas de color naranja. Este efecto nos proporciona una percepción la cual nos
hace creer que se está emitiendo luz desde esas partes del objeto. Simplemente es un efecto
óptico que en ningún caso llegara a ningún tipo de iluminación dinámica conocida.
2.2.3.6 Componente Mapa de Luces
La componente de Mapa de Luces viene a representar en una textura la luz que recibe ese
objeto del ambiente. Concretamente este efecto es muy costoso de calcular en tiempo real y
por eso, se considera como un efecto off-line o pre-calculado. Debido a esta limitación, los
mapas de luces se acostumbran a utilizar en objetos estáticos y que su posición no cambiará
con el tiempo.
Para entender este efecto, es necesario imaginarse como el color base del objeto es
multiplicado por el color del Mapa de Luces, aplicando de esta manera una iluminación global
pero estática al objeto en cuestión.
Para verlo más claro mostraremos a continuación una ilustración que nos servirá para ver el
efecto en funcionamiento.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 42 de 134
Ilustración 17 - Efecto del Mapa de Luces
En la ilustración anterior podemos observar como el color base, o lo que en su defecto para
nosotros será el color proporcionado por la componente Difusa, es multiplicado por el Mapa
de Luces. Este mapa nos indica donde hay luz y donde no la hay, asimismo también nos indica
de qué colores es la luz.
Hemos de tener en cuenta de que el hecho de aplicar este efecto nos condiciona a tener en el
objeto en concreto como mínimo un set de coordenadas de textura para el Mapa de Luces,
que será independiente de cualquier otro set que utilicemos en ese objeto.
2.2.3.7 Manager de texturas
El manager de texturas es una estructura de datos encargada de almacenar y gestionar las
texturas que nosotros necesitaremos durante nuestra aplicación.
Esta estructura nos permite almacenar todas las texturas que vayamos a utilizar con la
característica de mantener únicas las texturas almacenadas. Esto quiere decir que de cada una
de las texturas, solo tendremos una en memoria y solo una, no pudiendo estar repetida de
ningún modo.
Gracias a esta característica, aseguramos un consumo de memoria responsable a lo largo de la
vida de la aplicación a nivel de gestión de texturas. Por otro lado, el gestor de texturas nos da
acceso directo a estas texturas siempre que necesitemos de ellas.
Las texturas almacenadas en el gestor de memoria pueden ser de los formatos conocidos
habituales, esto implica a texturas de tipo BMP, JPG, TGA, PNG, DDS, etc. Para el programador
Difusa Mapa de Luces Resultado
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 43 de 134
que utilice este gestor, será completamente transparente cualquiera de los formatos sea
utilizado.
Ilustración 18 - Diagrama del Manager de Texturas
Como podemos observar en la ilustración anterior, solo existe una copia por cada textura en
memoria, al mismo tiempo existe un sistema de relación entre nombre de la textura y su Id
para que resulte mucho más fácil buscar la textura solicitada.
Asimismo podemos observar como a nivel de aplicación, ésta interactúa con el manager
siéndole transparente el funcionamiento interno de éste.
Textura 1 Nombre 1
Texturas en memoria
Relación Nombre - Id
Nombre 2
Nombre 3
Nombre 4
Textura 2
Textura 3
Textura 4
id
id
id
id
Manager de Texturas
Ap
licac
ión
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 44 de 134
2.2.4 Materiales
2.2.4.1 Introducción
Podríamos definir un material como una agrupación de texturas para intentar recrear un
efecto visual de la realidad.
Concretamente para nosotros, esta agrupación de texturas se compone de una textura de
cada una de las que anteriormente hemos hablado en el apartado de texturas. Esto supone
que cada uno de nuestros materiales tendrá una textura para ofrecer color de base al objeto,
una textura que nos ofrecerá información de las normales a nivel de píxel, una tercera textura
que nos ofrecerá información de brillos o componente especular a nivel de píxel, una textura
que nos ofrecerá una componente emisiva y para finalizar una quinta textura que nos dará
información de luz global estática aplicada a nuestro objeto.
Si observamos la ilustración siguiente, podremos comprobar una idea básica de lo que es en
nuestro motor 3D un material para un objeto.
Ilustración 19 - Estructura de un material
Como podemos observar en el diagrama anterior, un material está formado por parámetros de
dicho material y cinco texturas asociadas a ese material que lo definirán como tal en nuestro
motor.
Parámetros
Texturas
Material
Difusa Especular
Normal Emisiva
Mapa de Luces
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 45 de 134
Cada material tiene sus propios parámetros, estos parámetros le diferencian de los demás
dotándolo de características como escalado, desplazamiento y shader a utilizar.
2.2.4.2 Componentes de un material
Las componentes de una material son aquellas texturas que lo componen. Concretamente en
nuestros materiales tenemos cinco componentes, estas son: la componente Difusa, la
componente Especular, la componente Normal, la componente Emisiva y la componente de
Mapa de Luces.
Todas ellas en un material definen una textura a modo de poder ser utilizada en tiempo real
dentro del motor 3D. Si bien es necesario poner las cinco componentes para cada uno de los
materiales, cabe la posibilidad de que no necesitemos en todo momento alguna de las
componentes.
Por ejemplo se nos podría plantear el caso en que no necesitásemos la componente Emisiva a
la hora de representar en pantalla un objeto. Para paliar esta falta de necesidad, los materiales
utilizan unas texturas por defecto para no interferir en el modo de gestión de los shaders.
Estas texturas por defecto son texturas generadas para tal efecto las cuales son de diminutas
dimensiones, concretamente de 2 x 2 píxeles, para no sobrecargar el sistema con memoria
inútil.
2.2.4.3 Parámetros de un material
Los parámetros de un material definen como se comporta este en el motor 3D. Para ello aquí
tenemos un esquema de cómo se comportan los tres parámetros que podemos definir en un
material.
Escalado
o Este parámetro nos permite repetir las texturas de un material a lo largo de
una superficie tantas veces como queramos. Es útil para dotar de mayor
definición a superficies sin necesidad de cargar una textura muy grande en
memoria.
Desplazamiento
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 46 de 134
o Extremadamente útil para mover las texturas de un material a lo largo de una
superficie en la dirección que nosotros queramos. Hay que comentar que, al
igual como en el escalado, todas las texturas que conforman el resultado final
de la superficie se mueven a la misma velocidad.
Shader
o Con este parámetro le indicamos al material qué shader ha de utilizar para
renderizar dicho material.
2.2.4.4 Manager de materiales
El manager de materiales es aquel gestor encargado de gestionar los materiales que vamos a
tener en pantalla en todo momento.
Un material, cuando es definido por el artista o diseñador, toma un carácter de unicidad. Esto
quiere decir que no habrá más de un material igual dentro del manager de materiales al igual
como pasaba con las texturas.
Ilustración 20 - Diagrama del Manager de Materiales
Material 1
Materiales en memoria
Identificación
id
id
id
Manager de Materiales
Ap
licac
ión
Manager de Texturas
id Material 2
Material 3
Material 4
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 47 de 134
En la ilustración anterior podemos observar como el manager de materiales interactúa con el
manager de texturas para poder dar accesos a estas en todo momento. Paralelamente a todo
esto, la aplicación puede acceder a la interfaz tanto del manager de texturas como al de
materiales.
Esto nos proporciona un control global de los recursos a nivel de la aplicación, pues esta puede
decidir si acceder al manager para que este gestione materiales y texturas al mismo tiempo, o
por lo contrario, acceder al manager de texturas para obtener acceso a texturas individuales.
2.2.5 Render Targets
Normalmente cuando de aplicaciones 3D hablamos, siempre es necesaria una área de
memoria donde nuestra aplicación generara el resultado visual final y lo mostrará para que los
usuarios lo podamos ver.
A este proceso se le llama renderizar una escena y su finalidad es la de mostrar el resultado
por pantalla. El renderizado de la escena nos deja el resultado visual en una porción de
memoria del sistema llamada Back buffer. Este Back buffer es como una área de trabajo para
nuestra aplicación la cual monta la escena y nos muestra un resultado final
Fijémonos en el diagrama siguiente:
Ilustración 21 - Diagrama de renderizado
Renderizado
Back buffer
Front buffer
El renderizado utiliza el área de Back buffer como área de trabajo
Del Back buffer se pasa al Front buffer y este es mostrado al usuario
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 48 de 134
En la ilustración anterior podemos observar como el renderizado utiliza el Back buffer como
area de trabajo para que posteriormente sea mostrado al usuario.
A partir de aquí podemos definir como Render Target ese área de trabajo temporal que
nosotros le indicaremos a la librería gráfica para poder trabajar con más áreas de memoria.
Esto nos servirá para poder guardar o mantener información valiosa en forma de imagen para
posteriormente ser utilizada en procesos que lo requieran.
Ilustración 22 - Diagrama de renderizado con Render Targets
Fijémonos en la ilustración anterior como el renderizado puede utilizar el área del Back buffer
o por el contrario puede utilizar un Render Target definido por nosotros como área de trabajo.
Independientemente de cuál sea el área de trabajo, ésta podrá ser presentada ante el usuario
final como resultado del render.
Debe mencionarse que es muy recomendable renderizar la geometría de la escena junto a su
color original en el Back buffer y dejar a los otros Render Targets para procesado de otro tipo.
Esto es así debido a que el Back buffer tiene un sistema nativo de antialiasing que no es
soportado en los otros Render Targets y por tanto no es calculado en estos últimos.
Los Render Targets en una aplicación pueden ser configurados de muchas maneras. Se les
puede definir las dimensiones de ancho y alto, el tipo de píxel que será utilizado, si tienen o no
tienen buffer de profundidad, etc.
Renderizado
Back buffer
Front buffer
El renderizado utiliza el área de Back buffer como área de trabajo
Del Back buffer se pasa al Front buffer y este es mostrado al usuario
Render Target 1
Render Target 2
Render Target N
…
Los Render Targets pueden usarse para operaciones o pueden copiarse en el Back buffer para ser mostrados en pantalla
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 49 de 134
2.2.6 Efectos Especiales
2.2.6.1 Introducción
Los efectos especiales son esos efectos que se añaden a la escena aparte del color de base que
estamos dando a los objetos.
Efectos de este tipo podrían ser el efecto de la niebla y el efecto Glow. Estos efectos se añaden
a la escena para dotar a esta de una mayor integridad y realidad, aunque en muchas
ocasiones, estos efectos no son muy reales pero empujan las sensaciones de los usuarios hacia
estados donde nosotros queremos llegar.
Es de esperar entonces que gracias a estos efectos podamos transmitir sensaciones de calor,
frio, iluminación intensa, etc. Aunque éstas no sean completamente reales
2.2.6.2 Niebla
La niebla es un efecto visual recreado en las aplicaciones 3D que intenta igualar lo que nos
muestra en la vida real el efecto meteorológico de la niebla, es decir, un desvanecimiento de
los objetos a través de la distancia que nos separa de ellos.
Normalmente y debido a los factores reales que tiene la niebla en la vida real, ésta siempre
obtiene unos colores grisáceos tirando a blancos mediante los cuales se hace el
desvanecimiento de los objetos.
En nuestro motor 3D, hemos experimentado con este efecto meteorológico dotando a la
niebla de unos parámetros variables como color y distancias.
Las distancias en nuestra niebla nos indican dos valores muy importantes, el inicio del
desvanecimiento de los objetos en la niebla, y el desvanecimiento completo de estos.
Fijémonos a continuación en la ilustración siguiente como dependiendo de estos parámetros
se puede llegar a crear diferentes densidades de nieblas en una escena.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 50 de 134
Ilustración 23 - Diagrama de ejemplos de distancias de Nieblas
Como podemos observar en el diagrama anterior, existen dos distancias, la distancia en el que
la niebla empieza a ser presente, y la distancia en el que la niebla esta 100% presente y no se
puede distinguir nada.
Aparte de las distancias en la niebla, también tenemos otro parámetro con el que hemos
experimentado que ha sido el color de la niebla.
En un ambiente normal, la niebla tiene un solo color. En nuestro motor, la niebla tiene dos
colores, uno de inicio y otro final. Estos dos colores se aplican y son interpolados a través de la
niebla desde la distancia inicial y final de ésta.
Ilustración 24 - Diagrama de colores de una Niebla
Inicio Niebla Final Niebla
Color cercano Color lejano
Inicio Niebla Final Niebla
Near Clip Far Clip
Inicio Niebla Final Niebla
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 51 de 134
En la ilustración anterior podemos observar el experimento que llevamos a cabo en nuestro
motor 3D, es decir, el hecho de implementar una niebla que se basa en dos colores y no en
uno solo. Esto nos da más parámetros con los que jugar en una escena y nos permite
ambientar ésta de forma más flexible.
Si por el contrario deseamos una niebla como la de toda la vida, siempre podemos optar por
definir los dos colores de la niebla como el mismo color.
2.2.6.3 Glow
El efecto conocido como Glow o halos de luz son presentes en muchos sitios de la vida real y
proporcionan una visión enriquecida de las emanaciones de luz que existen en una escena
concreta.
Cuando hablamos de gráficos generados por ordenador, el brillo proporcionado por las luces
de éste son limitadas y no ofrecen los rangos que se pueden obtener en el mundo real. Es por
eso que los efectos de Glow son la única manera de entender una emanación de luz en un
mundo 3D como una fuente lo suficientemente potente de luz.
Concretamente el Glow de nuestro motor 3D proviene de dos fuentes paralelas las cuales lo
provocan. Estas fuentes son, los píxeles de mayor intensidad de un umbral y de la componente
emisiva que tienen todos los objetos que hemos descrito en apartados anteriores.
Ilustración 25 - Diagrama de flujo del proceso Glow
Como podemos observar en la ilustración anterior, existen las dos fuentes citadas para poder
realizar el Glow. Estas dos fuentes son combinadas para podérseles aplicar el proceso de Glow.
Píxeles con una intensidad superior a un
umbral
Componente
Emisiva
Proceso de Glow
Componente de Glow
resultante lista para aplicar
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 52 de 134
Finalmente obtenemos el Glow resultante que aplicaremos como post proceso en el render
final.
A continuación mostraremos de forma teórica cómo es generado el Glow a nivel de píxel.
Hemos de tener presente que el Glow se realiza normalmente en dos pasadas, una vertical y
otra horizontal. Veamos el diagrama siguiente:
Ilustración 26 - Proceso teórico de Glow a nivel de píxel
Como podemos observar en el diagrama anterior, el proceso de Glow teórico es realizado
mediante dos pasos, uno horizontal y otro vertical. Este proceso es así debido al mejor
entendimiento teórico de este mismo, pues en la realidad, no puede llevarse a cabo tal cual
porque en las tarjetas gráficas es imposible escribir en un píxel vecino mientras se está
tratando el píxel actual.
El primer paso es hacer la pasada horizontal, la cual extiende el color a los píxeles vecinos con menor intensidad según distancia.
El segundo paso es hacer lo mismo pero en vertical, es necesario partir del resultado anterior para un efecto correcto.
El efecto final es el color del píxel tratado esparcido por los píxeles vecinos, dando el efecto correcto de Glow o halo de luz.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 53 de 134
Debido a esto último, debemos entender como que un píxel no esparce su color a sus vecinos,
sino que este píxel, acumula una cantidad de color en sí mismo de sus vecinos. De esta manera
el píxel que estamos tratando lo que hace es acceder a sus vecinos y leer, que no escribir, su
valor de color y aplica un factor de multiplicación a éste para poder generar una media.
En nuestro motor 3D, hemos implementado un proceso de Glow que desarrolló en el año 2003
un señor que se llamaba Masaki Kawase (13). Este algoritmo parte de la base de intentar hacer
las dos pasadas en una sola, apoyándose en la capacidad que tienen las tarjetas gráficas hoy en
día de interpolar colores entre píxeles a la hora de leerlos de una texturas.
Lo que hace el algoritmo es recuperar el valor de un pixel ficticio que resulta estar entre medio
de dos píxeles de la textura. Como resultado obtenemos un valor interpolado entre píxeles
colindantes y obtenemos un efecto Glow que aunque no sea tan perfecto como en el caso
anterior, si que este último algoritmo hace que obtenemos mayor rendimiento.
Ilustración 27 - Diagrama de funcionamiento del Glow mediante Kawase
La lectura de píxeles se hace en el primer paso en la intersección de los píxeles colindantes. Esto nos da un color interpolado entre estos píxeles.
En el segundo paso, la lectura de píxeles se traslada a la siguiente intersección de píxeles, la cual nos da un color interpolado entre estos últimos.
En el tercer paso, ocurre exactamente lo mismo que en los anteriores dos, a excepción de que en cada paso sucesivo, el factor multiplicativo del color es menor.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 54 de 134
2.2.7 Estructura Pipe-Line
Ilustración 28 - Diagrama del Pipe-Line utilizado para renderizar
Occlusion Query
Frustrum Cube
ShadowMap
Sort Geometry
Render Cube
ShadowMap
Render Self
Ilumination
Render Skybox
Render Main
Geometry
Render Bright
Render Self + Bright
Glow Loop 1
Glow Loop 2
Render Main + Glow
Aplicamos una occlusion query a la geometría para solo tratar aquellos objetos que se ven.
Mediante frustrum culling elegimos y descartamos geometría para las 6 caras del Cube ShadowMap.
Ordenamos la geometría mediante materiales para que el impacto de rendimiento sea mínimo.
Renderizamos las 6 caras del Cube ShadowMap aplicando a cada pixel profundidad en vez de color.
Renderizamos la geometría de la escena mediante la componente emisiva de estas.
Renderizamos el SkyBox y nos aseguramos de que no se limpie el BackBuffer después.
Renderizamos la geometría de la escena principal en el BackBuffer sin haber sido limpiado antes.
Extraemos del render anterior mediante un umbral el brillo de los píxeles.
Sumamos los resultados del render Self más el render de Bright.
Aplicamos el algoritmo de Glow - Kawase con sus respectivas vueltas.
Juntamos todos los resultados anteriores para formar la imagen resultante.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 55 de 134
Como podemos observar en el diagrama anterior, la estructura pipe-line del renderizado en
nuestro motor sigue los pasos previamente mostrados.
Entre estos pasos podemos observar diversos grupos de acciones y procesos determinados por
diferentes colores, a continuación detallaremos sus funciones:
2.3 Tratamiento de eventos
2.3.1 Trama
Podemos definir una trama como un objeto contenedor de información la cual somos nosotros
quien decidimos su formato para su correcta interpretación.
Preparación de la geometría
o En estas etapas podemos observar como mediante algoritmos de
descarte y algoritmos de oclusión de geometría podemos determinar que
objetos estamos viendo y cuáles no. Gracias a este hecho, estamos
descargando al renderizado posterior de trabajo innecesario o
redundante.
Calculo de sombras
o Esta fase se caracteriza por el cálculo de sombras, este cálculo se hace
mediante la técnica Shadow Mapping, la cual hemos modificado e
implementado para que sea capaz de generar una sombra a partir de una
luz omnidireccional con degradado de intensidad según la distancia.
Renderizado principal
o En esta fase simplemente renderizamos la geometría mediante los
algoritmos de iluminación que hemos diseñado para nuestro motor 3D.
Post producción de efectos
o En esta fase se calculan los efectos de post producción como el Glow. Es
importante destacar que antes de esta etapa seria poco fructífero calcular
estos efectos.
Resultado final
o Es la suma de todos los procesos que hemos llevado a cabo.
Verde
Naranja
Azul
Lila
Rojo
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 56 de 134
En nuestro motor 3D, utilizamos las tramas como objetos para poder transportar información
allí donde nosotros la necesitamos. Estos objetos están diseñados para almacenar cualquier
tipo de dato y cualquier combinación de estos de una manera lineal y estructurada.
Una de las limitaciones en la naturaleza de este tipo de objetos es que debido a la infinidad de
posibles órdenes y tipos de datos distintos, seremos nosotros los programadores los
encargados de asegurar un protocolo de lectura coherente al orden de escritura de
información en dicha trama.
Si no se conserva este protocolo para cada tipo de caso que tengamos en un futuro, puede
derivar en la lectura de información corrupta debido a que no se ha respetado la estructura
interna de la trama que se ha marcado en su inicial escritura.
Veamos un ejemplo de cómo se respeta el orden en una trama:
Ilustración 29 - Diagrama de Lectura / Escritura en una trama
1002 1002
Escritura de un primer valor en la trama, este ocupa las primeras posiciones dentro de la trama.
Escritura de un segundo carácter dentro de la trama. Es un tipo de dato diferente pero se almacena de la misma manera.
‘A’
Escritura de un tercer miembro en coma flotante, que será introducido en la parte posterior de la trama.
Lectura del primer miembro, este miembro es un número y como tal se lee de la trama esperando ese tipo de dato.
Lectura del segundo miembro, pero en este caso, se lee un valor esperando que sea de otro tipo lo cual es un error.
4.352
1002
1002
1002
1002
‘A’
‘A’
‘A’
‘A’
4.352
4.352
4.352
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 57 de 134
Como podemos observar en el diagrama anterior, nosotros podemos insertar cualquier tipo de
dato en una trama, de hecho, es nuestro deber mantener el orden de lectura cuando
necesitemos recuperar algún dato.
Fijémonos en la primera lectura que hacemos, podemos observar como estamos leyendo un
tipo de dato que encaja perfectamente con el que hemos escrito en su inicio. Esto nos
devuelve el dato deseado de forma íntegra y sin errores.
Por el contrario, si realizamos una consulta como la segunda lectura del ejemplo, nos podemos
fijar que estamos intentando leer un tipo de dato que no concuerda con lo que se ha escrito
anteriormente en la trama. Esto desemboca en una lectura incoherente y corrupta del
segmento de datos leídos, pues estamos leyendo en este caso en particular un dato y parte del
siguiente.
2.3.2 Eventos
Los eventos los podemos definir como objetos con información que se generan cuando hay un
cambio de estado en algún sub sistema del motor 3D y que nosotros queramos tener
consciencia de ello.
En nuestro motor 3D, los eventos toman información de muchas partes y son encargados de
mantenerla hasta que es tratada debidamente. Un ejemplo de esto podrían ser las entradas de
teclado y ratón, la entrada de la red, un trigger, etc.
Un evento está formado por dos partes características: la primera es un identificador de
evento, y la segunda es una trama.
Identificador
o El identificador del evento es lo que nos permite saber de qué evento estamos
hablando. Estos identificadores tienen que ser definidos previamente por los
desarrolladores y llegar a un acuerdo sobre qué tipo de identificadores se
asignan a cada evento.
Trama
o Anteriormente hemos definido a la trama como ese contendor de información
ordenada por el desarrollador. En un evento, siempre tenemos
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 58 de 134
inherentemente una trama para almacenar la información que lleva ese
evento. Esto nos permite alimentar de información a un evento como por
ejemplo un evento de ratón, o por lo contrario tener eventos predefinidos en
el interior de un trigger y que este los haga activos cuando sea disparado.
A continuación mostramos un diagrama en forma de ejemplo de cómo podría ser un evento:
Ilustración 30 - Ejemplo de evento
Fijémonos de que el evento encapsula tanto el identificador como la trama que contiene la
información. Estos eventos son almacenados en una estructura de datos llamada Event
Manager, que al igual como anteriores managers se encarga de mantener un control y un
orden en los eventos que nos llegan y hemos de tratar.
2.3.3 Entrada / Salida
2.3.3.1 Teclado
Cuando hablamos de Entrada y Salida a nivel de teclado, nos referimos a los eventos que en
este caso entran generados por el teclado con la interactuación del usuario.
Para gestionar los eventos que se pueden hacer mediante un teclado, en nuestro motor hemos
implementado un gestor de eventos, que particularmente se le llama internamente tracker.
Este tracker es el encargado de gestionar cada uno de los eventos del teclado y de generar una
imagen virtual de cómo está el teclado en todo momento.
1002 ‘A’ 4.352 “Cadena”
Identificador
Evento
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 59 de 134
Gracias a este seguimiento, nosotros podemos comprobar en todo momento cuál es el estado
de cualquiera de las teclas que nosotros necesitamos. Esto ayuda a la hora de mantener una
idea de programación limpia y estructurada.
De igual modo que el tracker puede hacer un seguimiento exhaustivo del estado del teclado,
también se puede deshabilitar su función como tal. Esto es beneficioso en el caso en el que se
quiera bloquear toda entrada referente a teclado, es decir, dejaríamos al motor 3D sin entrada
posible mediante teclado.
2.3.3.2 Ratón
A nivel de Entrada y Salida, también tenemos un gestor encargado de mirar en todo momento
el estado del ratón.
El tracker del ratón, al igual como el del teclado, mira en todo momento el estado de éste para
poder ofrecer en tiempo de aplicación la información necesaria para un correcto
funcionamiento.
Este tracker a diferencia del de teclado, no solo ofrece el estado actual de los botones del
ratón sino que también ofrece información de las coordenadas actuales del puntero del ratón
como de sus incrementos diferenciales a través del tiempo.
Los incrementos diferenciales son necesarios para poder calcular bien los movimientos de la
cámara dentro del motor 3D. Así el cálculo diferencia ya viene hecho por el tracker de ratón y
no es necesario hacerlo en la cámara.
2.4 Escena
2.4.1 Vértices flexibles
Como ya podemos intuir, los objetos 3D representados por nuestro motor están hechos a
partir de triángulos. Cada triángulo por consiguiente tiene tres vértices. Los datos contenidos
en estos tres vértices son los encargados de decir cómo se renderizará este objeto en nuestro
motor 3D.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 60 de 134
La librería DirectX utiliza el formato FVF o Flexible Vertex Format que es un formato flexible
que nos permite definir la estructura de los vértices utilizados como nosotros necesitemos.
Esta definición flexible nos permite utilizar en cada caso una declaración de vértice acorde con
nuestras necesidades en ese momento. Podemos definir desde el vértice más simple con una
posición 3D o todo lo complicado que se requiera añadiendo datos e informando a la librería
de su estructura.
Ilustración 31 - Diferentes formatos de vértice
En el diagrama anterior podemos observar diferentes ejemplos del formato flexible del que
hablamos. Este formato nos proporciona la libertad necesaria para poder definir toda la
información que necesitemos para un vértice de nuestra geometría.
2.4.2 Vectores de índices y vértices
Cuando nosotros queremos representar un objeto 3D en pantalla, lo haremos mediante un
conjunto de triángulos que definirán su forma visual. Estos triángulos deberán estar
contenidos en una estructura de datos apta para ello.
Esta estructura de datos que hemos utilizado nosotros en nuestro motor 3D son los vectores
de índices y vértices.
Vértice
Posición
Vértice
Posición Color
Vértice
Posición Color
Normal
Vértice
Posición Color
Normal Tangente
Textura 1 Textura 2
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 61 de 134
Por cada objeto será necesaria la creación de dos estructuras de datos que irán
interrelacionadas, estas dos son el vector de índices y el vector de vértices. Gracias a esta
definición conjunta se nos asegura que los vértices contenidos en el vector de vértices serán
únicos para un objeto y que no estarán repetidos. Esto es debido a que el vector de índices es
el encargado de decir en cada momento que vértices forman cada triángulo.
Esta definición conjunta nos salvaguarda de problemas de optimización de memoria que hacen
referencia a la repetición de vértices debido a triángulos contiguos. Sí es cierto que habrá
índices repetidos a lo largo del vector de índices, pero de esta manera solo se repiten números
naturales e individuales y no estructuras de vértices flexibles que pueden llegar a representar
una carga de memoria importante.
Ilustración 32 - Index y Vertex Buffer interrelacionados
En la ilustración anterior podemos observar como un vector de índices se relaciona con un
vector de vértices. Podemos observar que existen índices repetidos para ciertos triángulos,
pero los datos representativos de un índice son significantemente inferiores en cantidad de
memoria con respecto a un vértice.
Vértice 1
Vértice 2
Vértice 3
Vértice 4
Vértice 5
Vértice 6
1 2 3 4 5 6 7 8 9
Triángulo 1 Triángulo 2 Triángulo 3
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 62 de 134
2.4.3 Mallas
2.4.3.1 Introducción
A partir del punto anterior, hemos entendido que los triángulos como figura geométrica son
almacenados en estructuras lógicas llamadas Vertex Buffers.
Gracias a estas estructuras lógicas, nosotros podremos agruparlas para poder llegar a formar lo
que nombraremos una malla. Las mallas son utilizadas en nuestro motor 3D para poder
representar gráficamente a los objetos en pantalla. Estas mallas son el objeto mínimo que el
motor 3D puede dar acceso a la aplicación en cuanto a representación gráfica se refiere.
2.4.3.2 Partes de una malla
Como hemos comentado anteriormente, una malla está compuesta por vectores de vértices y
vectores de índices. Las partes de una malla precisamente son la agrupación de estos vectores
por duetos que son formados por un vector de vértices y otro de índices.
Aparte de los vectores de vértices y índices que formarán la malla gráfica, también existe otro
tipo de malla que coexiste con la malla gráfica, ésta es la malla física.
La malla física es una malla de mucha más simplicidad a nivel de forma gráfica, pues ésta
contiene muchos menos triángulos sacrificando detalles geométricos. Esta malla física es
utilizada mediante un algoritmo antes de hacer nada en el render gráfico. Esto nos permite
descartar geometría mediante sus oclusiones visuales.
Hemos de remarcar que en cada malla solo puede haber una malla física, pues es totalmente
lógico que una malla sea representada mediante una sola malla física. Por el contrario, si
representásemos una malla mediante varias mallas físicas, lo único que conseguiríamos sería
perder rendimiento a la hora de calcular las oclusiones.
A continuación podemos observar como una malla es representada a nivel lógico dentro del
motor 3D.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 63 de 134
Ilustración 33 - Diagrama lógico de una malla
En el diagrama anterior podemos ver los dos grupos predominantes, es decir, la malla gráfica
de la que hemos hablado con anterioridad, y la malla física, que solo hay una para cada malla
que tengamos en nuestro motor 3D.
Podemos observar que en la malla gráfica existen varios duetos de vectores de índices y
vértices. Estos duetos forman un conjunto de mallas que en su conjunto formaran el resultado
final de la malla. Esto quiere decir que una malla puede ser representada en pantalla mediante
la representación de muchas mallas.
2.4.3.3 Manager de mallas
El manager de mallas, al igual como otros managers que ya hemos comentado anteriormente,
se encarga de gestionar las mallas que podemos tener en nuestro motor 3D. De este modo, la
aplicación interactúa con el manager para poder cargar en tiempo real las mallas y así poder
ser mostradas en pantalla.
Un aspecto importante del gestor de mallas es que tiene acceso directo a otros managers que
ya hemos descrito como el manager de texturas y el manager de materiales. Gracias a este
acceso directo, el manager de mallas puede cargar y descargar toda la información necesaria
Malla
Vector de Índices Vector de Vértices
Vector de Índices Vector de Vértices
Vector de Índices Vector de Vértices
…
Malla Gráfica
Malla Física
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 64 de 134
tanto a nivel de texturas como de materiales siéndole este proceso transparente al
desarrollador.
Cuando un manager de mallas recibe la orden de cargar una malla en memoria, éste lee el
archivo para rescatar la información de la malla, interactúa con el manager de materiales para
poder cargar los materiales para esa malla, y al mismo tiempo, el manager de materiales
interactúa con el manager de texturas para poder cargar las texturas necesarias.
Asimismo, cuando el manager de mallas decide descargar de memoria una malla, este
manager, aparte de descargar la información lógica de la malla de memoria, también accede al
manager de materiales para indicarle que materiales debe descargar, y este último hace lo
mismo con el manager de texturas, el cual descarga de memoria esas texturas necesarias.
Ilustración 34 - Diagrama del Manager de Mallas
Malla 1
Mallas en memoria
Identificación
id
id
id
Manager de Mallas
Ap
licac
ión
Manager de Materiales
id Malla 2
Malla 3
Malla 4
Manager de Texturas
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 65 de 134
2.4.4 Submallas
2.4.4.1 Introducción
En un diagrama anterior hemos podido ver que una malla se dividía entre duetos de vectores
de índices y vectores de vértices y una malla física necesaria para poder calcular oclusiones
visuales.
Una submalla en nuestro motor 3D se define como un dueto entre un vector de índices y un
vector de vértices. Este dueto, que a partir de ahora llamaremos submalla, es el que mediante
su presencia, y la de otras submallas más, nos permite crear una malla completa y visual.
2.4.4.2 Partes de una submalla
Como podemos prever, las partes de una submalla vienen dadas por un primer vector de
índices seguido de un vector de vértices que serán indexados por estos índices.
Estos dos vectores conformaran la parte geométrica y la que mostrara en pantalla la forma 3D
de la malla.
Por otro lado tenemos que para estos dos vectores, siempre tenemos asociado un material.
Esto quiere decir que una malla está formada por varias submallas, y cada una de esta está
asociada a un material en concreto.
Por lo tanto podemos tener en pantalla objetos complejos que a lo largo de su superficie el
color y material de ésta cambie según las zonas.
Con esta información, podemos actualizar el diagrama que hemos visto con anterioridad y
remarcar lo que es una submalla dentro de una malla
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 66 de 134
Ilustración 35 - Especificación de la Submalla
Una vez tenemos descrito cuales son las partes de una submalla, solo nos queda remarcar
cómo se comporta ésta en el proceso de renderizado.
Una submalla es el elemento atómico más pequeño que el renderizado acepta para poder ser
tratado. Esto es así ya que cada submalla tiene asociado un material, y el proceso de
renderizado trata los objetos según el material.
Gracias a esta información podemos comprobar como una malla es descompuesta en
submallas, y estas tratadas y renderizadas independientemente.
Este proceso se lleva a cabo mediante esta idea ya que es beneficioso a nivel de renderizado
que todas las mallas que comparten un mismo material sean renderizadas una detrás de otra y
no alternando con otros materiales, pues una de las cosas que penaliza más en la librería
gráfica es la de hacer cambios en los materiales.
2.4.4.3 Manager de submallas
El manager de submallas lo podríamos definir como ese manager intermedio que es accedido
por el manager de mallas y al mismo tiempo accede a otros managers como el de texturas o el
de materiales.
Malla
Vector de Índices Vector de Vértices
Vector de Índices Vector de Vértices
…
Malla Gráfica
Malla Física
Material
Vector de Índices Vector de Vértices Material
Submalla
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 67 de 134
Este manager es el encargado de almacenar las submallas, que serán el elemento más
accedido por el bucle de render. Aparte de esto, también es necesario comentar que este
manager es totalmente consistente con la metodología de los managers pues cuando el
manager de mallas indica que una malla debe ser borrada, el manager de submallas borra
todas las submallas asociadas a esa malla.
Ilustración 36 - Diagrama del manager de Submallas
En el diagrama anterior podemos observar como el manager de mallas accede al manager de
submallas donde están todas estas almacenadas, al mismo tiempo y como ya es normal, estos
managers tienen acceso a otros managers para tener acceso a la información necesaria.
Submalla 1
Submallas en memoria
Identificación
id
id
id
Manager de Submallas
Ap
licac
ión
Manager de Materiales
id Submalla 2
Submalla 3
Submalla 4
Manager de Texturas
Manager de Mallas
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 68 de 134
2.4.5 Bounding Boxes
Una Bounding Box es la mínima caja geométrica en la que puede llegar a caber un modelo
determinador.
Partiendo de esta definición, una Bounding Box, mediante unas coordenadas, crea una caja
imaginaria que engloba la totalidad de un modelo de nuestro motor 3D.
Gracias a este hecho, es posible simplificar un objeto mediante su Bounding Box para así
aligerar la carga en procesos lógicos de exclusión o procesado de objetos en tiempo real.
La orientación de las Bounding Boxes de nuestro motor 3D está alineada con los ejes del
mundo, es decir, en ningún caso vamos a tener una caja rotada de este estilo. Con esta
premisa de orientación alineada, es mucho más fácil presuponer información a la hora de
hacer los cálculos asociados con colisiones.
A continuación veremos una representación visual de lo que es una Bounding Box:
Ilustración 37 - Ejemplos de Bounding Boxes
En la ilustración anterior podemos observar como los objetos están contenidos dentro de unas
cajas representadas mediante líneas. Estas cajas nos ayudan a poder representar los objetos
en los algoritmos mediante una representación más sencilla y más fácil de calcular.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 69 de 134
2.4.6 Cámaras
Cuando nosotros montamos una escena y la renderizamos para que sea mostrada por pantalla
como resultado final, aparte de las luces, los modelos y efectos especiales, necesitamos unos
parámetros de vital importancia.
Estos parámetros son las matrices de Mundo, Visión y Proyección. Estas matrices nos definen
en todo momento lo siguiente:
Mundo (World)
o Esta matriz nos multiplica y suma las coordenadas de nuestros modelos para
así poder escalar, rotar o trasladar el mundo al completo en el que estamos. Se
utiliza muy poco debido a que muy pocas veces es necesario modificar el
mundo en su globalidad como lo conocemos nosotros.
Visión (View)
o Esta matriz nos ayuda a poder crear unos valores que multiplicados por las
demás matrices nos dará un punto de visión, una dirección en la cual miramos
y una orientación de nuestro ojo de visión.
Proyección (Projection)
o Esta matriz al igual que la de visión, nos crea unos valores que multiplicados
por las otras dos matrices, nos definirá un volumen de visión partiendo del
ángulo de apertura de la cámara y de la distancia de visión mínima y máxima.
El conjunto de estas matrices, como decíamos anteriormente, nos permite generar una escena
desde el punto de visión que nosotros escojamos, con el ángulo que nosotros queramos y con
la profundidad y escalado deseado.
Cada vez que queramos renderizar la escena desde un punto de visión diferente, será
necesario cambiar los valores de estas tres matrices a los deseados. Es por ese motivo por el
cual se crea un objeto controlador al cargo de estas tres matrices que a partir de ahora
llamaremos cámara.
Esta cámara es la encargada de decirnos en todo momento cuáles son esas tres matrices de las
que hemos hablado anteriormente y, al mismo tiempo, nos permite mover, girar, abrir o cerrar
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 70 de 134
en ángulo, etc. de una forma más cómoda que si se tratara de modificar las matrices
directamente.
Una vez hemos hablado sobre qué es lo que engloba a nivel teórico una cámara, hablaremos
de los dos tipos de cámaras más comunes y de cuál de ellas hacemos uso en nuestro motor.
Primera persona (1P)
o Una cámara en primera persona es esa cámara que emula nuestra visión real
del día a día. Esto se refiere a que esta cámara tiene el punto de visión fijo, y el
punto hacia donde mira es el móvil. Por lo tanto, es la cámara que según su
naturaleza, más se asemejaría a la visión de una persona humana.
Tercera persona (3P)
o Las cámaras de tercera persona reciben su nombre debido a que su forma de
ver a través de ellas es como la de una persona que está mirando fijamente al
punto que es protagonista en este momento. Este aspecto desplaza a la
cámara y la deja en un segundo plano, es decir, esta cámara siempre estará
observando a un cierto punto sin elección de ningún otro.
A continuación veremos un diagrama en el cual podremos observar los dos tipos de cámaras y
como estas son representadas en 3D.
Ilustración 38 - Diagrama de cámaras en primera y tercera persona
En la ilustración anterior podemos observar las diferencias entre la primera y la tercera
persona. Mientras la primera persona, goza de un movimiento esférico para poder mirar
Primera persona Tercera persona
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 71 de 134
cualquier punto desde él, la tercera persona se mueve ella misma a lo largo de la esfera para
estar siempre mirando al punto central de dicha esfera.
2.4.7 Frustrums
Por Frustrum podemos entender el volumen que se encierra dentro de una figura,
normalmente una pirámide o cilindro delimitada por dos planos paralelos, denominados Near
Plane y Far Plane.
De esta definición podemos ver que en nuestras aplicaciones 3D, la forma natural más apta
para nosotros es la de una pirámide, normalmente llamada pirámide de visión. Esta pirámide
está formada por seis planos, estos planos nos determinaran un volumen interior donde la
geometría que esté en ese volumen se renderizará, en cambio, la geometría que quede fuera
de ese volumen no se renderizará.
A continuación podemos observar una ilustración con este concepto para un entendimiento
más fácil.
Ilustración 39 - Volumen de un Frustrum
Como se puede observar en la ilustración anterior, los cilindros verdes (C, D, E, F) están dentro
del volumen mientras que los cilindros naranjas (A, B, J, I, G, H) no lo están.
J
A
B
C D E
G H
F I
Far Plane
Near Plane
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 72 de 134
Los seis planos que conforman la pirámide de visión, pueden extraerse de las matrices que
hemos visto anteriormente para generar la escena, es decir, de las matrices Mundo, Visión y
Proyección.
Concretamente en nuestro motor 3D obtenemos los planos de la matriz resultante de la
multiplicación de la matriz de Visión y la matriz de Proyección. Esto nos dará los planos en el
espacio Mundo y los coeficientes de los planos se extraerán de la siguiente forma:
Plano Coeficientes de Plano
Izquierda
Derecha
Arriba
Abajo
Cercano
Lejano
Tabla 11 - Coeficientes de los planos del Frustrum
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 73 de 134
Una vez nosotros tenemos los coeficientes de los seis planos que conformarán nuestro
volumen de visión es relativamente fácil determinar si un punto 3D está por delante o por
detrás de dichos planos, pues solo hay que substituir las coordenadas del punto (X, Y, Z) en las
ecuaciones de los planos. Si el valor resultante es positivo, está delante del plano, de lo
contrario está por detrás.
Gracias a esta técnica, si un punto está por detrás de los 6 planos, implica que no está dentro
del volumen de visión, por lo tanto, no es necesario que lo pintemos ni lo tratemos.
Concretamente aquí entendemos el porqué es recomendable asociar una geometría más
simple que la del propio objeto, es decir, una Bounding Box, ya que de este modo nos será más
fácil y más rápido descartar si un modelo se está viendo o no.
2.5 Sombras
2.5.1 Introducción
Cuando hablamos sobre sombras en una escena 3D no nos referimos a la falta de luz en una
superficie debido al ángulo de ésta con el del punto que emana luz. Nos referimos a la oclusión
de luz que un objeto realiza a otro objeto en tiempo real.
De tipos de sombras se han diseñado muchas y muy variadas según lo requiera la escena.
Tenemos desde la sombra más fácil, que es colocar a los pies del objeto un círculo a forma de
sombra, hasta los métodos más complicados de hoy en día que generan sombras con perfil
difuminado y con calidades visuales cada vez más elevadas.
La evolución de las sombras en los juegos ha pasado por sombras de carácter simple donde la
sombra tenía forma de círculo o de perímetro que era estático en el suelo y se mantenía
centrado en el objeto.
Años después surgiría un método para poder generar sombras proyectando los contornos de
los objetos desde el origen de la luz hasta el infinito, llamándose este método sombras
volumétricas. Estas últimas gozan de un error natural debido a que los contornos de la sombra
son extremadamente definidos y chocan con las sombras reales dando un aspecto más alejado
de la realidad de lo que se pretende.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 74 de 134
Poco a poco el mundo de las sombras evolucionó y muchos programadores se han movido a
un tipo de sombras llamado shadow mapping, donde las sombras son realizadas gracias a una
textura generada intencionadamente de distancias entre el origen de la luz y la geometría a
tratar.
El hecho de que se tenga una textura gracias a la cual se puede generar la sombra, da la
capacidad al programador de desarrollar algoritmos basados en texturas y por consiguiente en
espacios 2D los cuales nos permiten aplicar degradados y difuminados a lo largo de los
contornos deseados.
Concretamente en nuestro motor 3D hemos implementado una técnica de sombras como la
que hemos descrito anteriormente como shadow mapping, pero a diferencia de que en luces
como las spot light que solo necesitan una textura para generar la sombra, nosotros hemos
utilizado un cubemap para poder generar sombras con una luz omnidireccional.
2.5.2 Cube Shadow Maps
Como ya hemos dicho, nuestro algoritmo utiliza un cubemap en el cual almacenaremos
distancias en lugar de información de color en los píxeles de éste. A continuación veremos la
estructura de un cubemap para tener en mente como está representado en memoria.
Ilustración 40 - Estructura de un cubemap
X
-Z
Cara Y
X
Y
Cara Z
X
Z
Cara -Y
-Z
Y
Cara X
-X
Y
Cara -Z
Z
Y
Cara -X
U
V
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 75 de 134
Como podemos observar en la ilustración anterior, el cubemap está compuesto de seis caras,
estas caras son cada una de ellas texturas del tipo y formato que nosotros queramos, al igual
como las dimensiones de estas.
Una vez hemos visto como un cubemap está estructurado internamente, hablaremos de cómo
accederemos a la información de un cubemap.
En un cubemap, a diferencia de los accesos 2D que se hacen en texturas normales, aquí es
necesario acceder a través de un acceso 3D, o lo que es lo mismo un vector 3D. Gracias a estos
vectores seremos capaces de recuperar el color o el dato deseado almacenado en ese punto
por el cubemap.
A continuación presentamos una ilustración de cómo los accesos desde un cubemap son
realizados y cómo los cubemap devuelven el color o dato asociado al punto donde accedemos.
Ilustración 41 - Ejemplo de acceso a la información de un cubemap
Xres
Xres
Xre
s
Xre
s
Xres
Xres
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 76 de 134
En la ilustración anterior vemos como se recupera la información de un cubemap para poder
operar con ella.
Habiendo visto esto, solo nos queda explicar cómo se calculan las sombras. Para hacer dichas
sombras, es necesario renderizar la escena desde la posición de la luz omnidireccional seis
veces, una en cada una de las direcciones que marca el cubemap.
Estos múltiplos renders de la escena tienen la peculiaridad de que no sacan un valor
interpretado como color, sino que nosotros en cada uno de los píxeles de las caras del
cubemap almacenamos la distancia desde la posición de la luz hasta dicho píxel.
Una vez tenemos el cubemap con las distancias, solo tenemos que hacer una comparación
cuando estamos renderizando desde la cámara desde donde miramos.
Es decir, si el pixel que está iluminado desde la luz omnidireccional tiene una distancia mayor a
la que se puede ver desde el cubemap en esa dirección, entonces el píxel que estamos
tratando está en oclusión de esa luz, y eso implica que está en sombra, si no el píxel se
encuentra iluminado normalmente.
Ilustración 42 - Ejemplo de aplicación de sombras
La luz no obtiene ninguna obstrucción en éste píxel, aunque no se vea desde la cámara. Este píxel está iluminado.
La distancia de este píxel es más grande que la distancia del píxel que se ve desde la luz. Este píxel esta en sombra.
La distancia de éste píxel es la misma que la distancia desde la luz. No hay obstrucciones por lo tanto está iluminado.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 77 de 134
2.6 Exportador
El exportador, llamado Constructor en el proyecto, es una de las aplicaciones mas criticas que
tiene este proyecto. Esto se debe a que sin él, nosotros no seriamos capaces de obtener los
modelos formateados de una forma específica para poderlos representar por pantalla.
Es por eso que el exportador obtiene una relevancia muy elevada en este proyecto, a la altura
de factores como la ecuación de iluminación o el diseño final de la demo.
Para poder entender el trabajo del exportador, es necesario entender cuáles son las dos partes
que queremos interconectar, pues el exportador será un software que obtendrá datos de una
parte y los transformará para la otra parte.
Por un lado tenemos nuestro software de diseño 3D, con el que somos capaces de hacer
nuestros propios modelos. Por otro lado tenemos nuestro motor 3D que estamos
desarrollando, y que como tal, necesita los modelos 3D formateados de una manera específica
para que éste pueda entenderlos.
Ilustración 43 - Partes del software a unir
Como podemos observar en la ilustración anterior, tenemos dos aplicaciones importantes que
necesitan ser unidas por un exportador para que las dos puedan coexistir y operar de manera
coordinada.
De no ser así, nos veríamos envueltos en una tarea de proporciones titánicas, ya que el motor
3D necesita mucha información para cada objeto, y dichos objetos no salen con esa
información directamente desde el software de edición 3D Studio MAX.
A continuación detallaremos todas las fases que conforman el exportador y por las cuales pasa
la información del 3D Studio MAX hasta convertirse en información útil para el motor 3D.
3D Studio MAX
iL Engine
?
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 78 de 134
Fases del Constructor
Fase 1
En la primera fase, la información del 3D Studio es exportada al exterior de
este programa mediante un formato abierto y en forma de texto plano
llamado ASE. Este formato aunque tenga muchísimas carencias nos servirá
para poder leerlo de manera cómoda por nuestro exportador.
Fase 2
En la segunda fase, el fichero ASE es parseado y recolocado en el interior de
las estructuras de datos del exportador para poder tener la información de
este archivo en un valor lógico con el cual se pueda operar de manera sencilla
y eficiente.
Fase 3
En la tercera fase de este exportador, la información lógica ASE es
transformada en información donde su estructura concuerde con las
definiciones de Vertex Buffer y Index Buffer. Por otro lado también se
reconfiguran las normales a dichos vértices ya que el formato ASE no lo hace
por sí solo.
Fase 4 En la cuarta fase, estos Vertex e Index Buffers son optimizados y purgados de
información innecesaria que pueda proceder de la fase anterior.
Fase 5
En la quinta fase, se procede al fusionado de dos objetos, esto es debido a que
desde el 3D Studio MAX no nos es posible exportar directamente un objeto
con doble set de texturas. Gracias a esta fase, podemos juntar dos objetos que
coincidan en estructura para dar como resultado un objeto con estructura
idéntica pero que conserve los sets de texturas de los dos objetos.
Fase 6
En la sexta fase, cada uno de los objetos resultantes es analizado y se procede
al cálculo de sus tangentes por vértice. Gracias a que podemos calcular este
dato en modo off-line, nos ahorramos tener que hacerlo en la carga de
modelo.
Fase 7
En esta séptima fase, el objeto resultante es fusionado otra vez con otro
objeto, pero este ahora es el objeto que concuerda con la estructura física que
tendrá dicho objeto para usarla dentro del motor 3D.
Fase 8 En esta última fase, escribimos a disco toda la información que hemos llevado
a cabo con las fases anteriores.
Fase 9 Esta fase es totalmente opcional, y existe para poder exportar no geometría
como tal sino líneas 3D que nos serán útiles para hacer caminos u otras cosas.
Tabla 12 - Fases del Constructor
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 79 de 134
Ilustración 44 - Diagrama de las fases del Constructor
Como podemos observar en la ilustración anterior, las fases del Constructor son secuenciales
aunque existen algunas de ellas que son totalmente opcionales, y dependen exclusivamente
de qué tipo de datos ha exportado el documento ASE.
Fase 1 Exportar la
geometría a formato ASE
Fase 2 Parseado del fichero ASE
Fase 3 Cálculo de
ASE a Vertex y Index Buffer
Fase 9 Exportación de líneas 3D
Fase 9.1 Escritura en disco de las
líneas 3D
Fase 4 Optimización de Vertex e
Index Buffers
Fase 5 Fusionado
para dos sets de texturas
Fase 6 Cálculo de tangentes
Fase 7 Fusionado con objeto
físico
Fase 8 Escritura en
disco
3D Studio MAX
iL Engine
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 80 de 134
3 Parte práctica
3.1 Librería gráfica
3.1.1 DirectX 9.0c
Lo primero que tenemos que tener en cuenta para la inclusión de la librería gráfica son las
instrucciones para poder añadirla al proyecto. Estas instrucciones engloban tanto a la propia
librería como a librerías de carácter útil a lo largo de nuestro desarrollo.
Librerías de DirectX 9.0c
#include <Windows.h>
#include <mmsystem.h>
#include <d3dx9.h>
Tabla 13 - Librerías necesarias para DirectX 9
Una vez tenemos incluidas las librerías necesarias para invocar a los objetos de DirectX al igual
como para poder hacer cálculos de matrices mediante optimizaciones de sistema y también
poder hacer referencias a los objetos de Windows para así poder crear ventanas y controles,
es necesario empezar con la inicialización de los datos necesarios para la aplicación en sí.
Código par inicializar DirectX 9.0c
// Creamos el objeto D3D.
if( NULL == ( g_pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) )
return E_FAIL;
// Inicializamos la estructura para crear el objeto D3DDevice.
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory( &d3dpp, sizeof(d3dpp) );
d3dpp.Windowed = TRUE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
d3dpp.EnableAutoDepthStencil = TRUE;
d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
// Creacion del D3DDevice
if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
hWnd, D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &g_pd3dDevice ) ) )
{
return E_FAIL;
}
Tabla 14 - Inicialización de DirectX 9.0c
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 81 de 134
Una vez hemos inicializado el objeto que nos contendrá los datos de DirectX y hemos puesto
los parámetros adecuados para nuestra aplicación, solo nos queda el hecho de implementar el
bucle de programa para que nuestra aplicación pueda ejecutarse.
Código del bucle de renderizado
// Registramos la clase de ventana.
WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, MsgProc, 0L, 0L,
GetModuleHandle(NULL), NULL, NULL, NULL, NULL,
L"iL engine", NULL };
RegisterClassEx( &wc );
// Create mos la ventana de aplicacion.
HWND hWnd = CreateWindow( L"iL engine", L"iL engine : Demo",
WS_OVERLAPPEDWINDOW, 100, 100, 300, 300,
NULL, NULL, wc.hInstance, NULL );
// Inicializamos Direct3D
if( SUCCEEDED( InitD3D( hWnd ) ) )
{
// Creamos la geometria
if( SUCCEEDED( InitGeometry() ) )
{
// Mostramos la ventana
ShowWindow( hWnd, SW_SHOWDEFAULT );
UpdateWindow( hWnd );
// Entramos en el bucle de mensajes
MSG msg;
ZeroMemory( &msg, sizeof(msg) );
while( msg.message!=WM_QUIT )
{
if( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) )
{
TranslateMessage( &msg );
DispatchMessage( &msg );
}
else
Render();
}
}
}
UnregisterClass( L"iL engine", wc.hInstance );
return 0;
Tabla 15 - Bucle de renderizado de la aplicación
Como podemos comprobar en la última tabla, el bucle de renderizado está gobernado por los
mensajes que el sistema operativo Windows nos manda. Este bucle solo parará en este caso
cuando el mensaje de salida de una ventana sea mandado.
Igualmente también podemos observar como el renderizado de la aplicación sólo se realiza
cuando el bucle no ha recibido ningún mensaje, de lo contrario, se destina tiempo a tratar e
interpretar el mensaje y en la siguiente vuelta se sigue renderizando.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 82 de 134
3.1.2 DXUT DXUT, como ya hemos comentado en la parte teórica, es una capa de software que nos ayuda
a la hora de gestionar nuestra aplicación. Esta ayuda nos viene dada mediante el control del
bucle de programa y de todos los mensajes que vienen dados por Windows y que modifican a
nuestra aplicación.
Librerías de DXUT
#include "DXUT.h"
#include "DXUTcamera.h"
#include "DXUTsettingsdlg.h"
#include "SDKmesh.h"
#include "SDKmisc.h"
#include "resource.h"
Tabla 16 - Librerías necesarias para DXUT
En la anterior tabla podemos observar cuales son las librerías necesarias que hemos de incluir
en nuestro proyecto para que este pueda gozar de la ayuda que ofrece DXUT en gestión de la
aplicación.
Callbacks de DXUT
// Iniciamos los callbacks de DXUT
DXUTSetCallbackD3D9DeviceAcceptable( IsDeviceAcceptable );
DXUTSetCallbackD3D9DeviceCreated( OnCreateDevice );
DXUTSetCallbackD3D9DeviceReset( OnResetDevice );
DXUTSetCallbackD3D9FrameRender( OnFrameRender );
DXUTSetCallbackD3D9DeviceLost( OnLostDevice );
DXUTSetCallbackD3D9DeviceDestroyed( OnDestroyDevice );
DXUTSetCallbackMsgProc( MsgProc );
DXUTSetCallbackFrameMove( OnFrameMove );
DXUTSetCallbackDeviceChanging( ModifyDeviceSettings );
Tabla 17 - Inicialización de los callbacks de DXUT
Una vez introducidas las librerías, a continuación y cómo podemos observar en la tabla
anterior, necesitamos informarle a la librería DXUT de donde están las funciones que ésta
necesita para poder operar como es debido. La naturaleza de los callbacks es que nosotros
definiremos unas funciones de control y DXUT las llamará cuando sea necesario.
De esta manera podemos observar como la ayuda de DXUT se hace patente pues el bucle de
renderizado queda algo más oculto en el código y nosotros solo tenemos que centrarnos en
dichas funciones de control.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 83 de 134
Una vez inicializadas todas las funciones que estarán ligadas a los callbacks de DXUT, es
necesario invocar el bucle de programa desde DXUT. Esto se hace de la siguiente forma:
Bucle de programa
//Inicializacion de la interfaz visual de la aplicacion
InitApp();
//Inicializacion de nuestro motor 3D
m_Engine.InitializeEngine();
//Invocamos el bucle de programa desde DXUT
DXUTMainLoop(); Tabla 18 - Bucle de programa de DXUT
Cuando queremos cerrar nuestra aplicación y ya hemos salido del bucle de programa, es
necesario hacer una última cosa, y es invocar a una función para que nos devuelva el código de
salida de nuestra aplicación. A continuación indicamos como.
Código de salida
//Devolvemos el codigo de salida de nuestra aplicacion
return DXUTGetExitCode(); Tabla 19 - Retorno del código de la aplicación
Una vez hemos visto las partes que necesitamos de DXUT para tener lista nuestra aplicación,
hemos de comentar que de todas las ayudas que nos ofrece DXUT, nosotros solo hemos hecho
uso de dos de ellas.
Callbacks
o Como ya hemos comentado, los callbacks nos han ayudado para esconder el
bucle de programa y así poder centrarnos en nuestro motor y no tanto en la
gestión de dicho bucle.
Interfaz Gráfica de Usuario (GUI)
o Hemos usado la interfaz gráfica que nos proporciona DXUT, de este modo no
tenemos que implementarla nosotros ya que cumple con creces su función en
nuestra aplicación. Asimismo también cabe comentar que no está en los
planes de este proyecto implementar una interfaz gráfica para usuario.
Con estas dos ayudas que hemos usado de DXUT ya podemos cerrar la lista. DXUT ofrece
infinidad de ayudas mas como por ejemplo implementaciones de clases útiles, formatos de
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 84 de 134
fichero, etc. Aunque hemos de destacar que este proyecto parte de la idea del aprendizaje y
por eso hemos optado por implementar todo lo posible desde sus inicios.
3.2 Renderizado
3.2.1 Iluminación Para poder comprender la iluminación que se realiza en este motor 3D, hemos de
remontarnos al apartado de iluminación de la parte teórica. Una vez recordada la educación
de iluminación solo nos falta comentar que esta ecuación ha sido implementada en los shaders
de este motor 3D. A continuación explicaremos paso a paso esta ecuación y cómo se lleva a
cabo.
Definición de una textura
// -------------------------------------------------------------------
// Texture Samplers
// -------------------------------------------------------------------
texture DiffuseTexture;
sampler DiffuseSampler = sampler_state
{
Texture = ( DiffuseTexture );
//Filtrados
MIPFILTER = LINEAR;
MAGFILTER = LINEAR;
MINFILTER = LINEAR;
}; Tabla 20 - Variable de textura en el shader
Para empezar con la explicación de la iluminación, debemos comentar cómo se definen las
variables que se necesitaran durante todo el proceso, aquí podemos ver un ejemplo de
definición de una variable de tipo textura. En los shaders son muy habituales estos tipos de
variables y gracias a ellos podemos recuperar colores de dicha textura.
Definición de variables lógicas
// -------------------------------------------------------------------
// Structs
// -------------------------------------------------------------------
uniform float4x4 world_view_proj;
uniform float3 light_pos;
uniform float4 light_color;
uniform float light_pendent;
uniform float light_offset;
//... Tabla 21 - Variables de tipo lógico
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 85 de 134
Como podemos observar en la tabla anterior, la definición de las variables lógicas de tipo
valores reales es tan necesaria como cualquier otra cosa, pues en éstas vienen definidos
parámetros tan importantes como la posición de las luces, el color de estas, o la matriz que
agrupa la matriz de mundo, la de visión y la de proyección.
Estructura de salida del Vertex Shader
struct VS_OUTPUT
{
float4 pos : POSITION;
float2 tex0 : TEXCOORD0;
float2 tex1 : TEXCOORD1;
float3 pos_world : TEXCOORD2;
float3 light_pos_to_px : TEXCOORD3;
float3 h : TEXCOORD4;
float3 norm : TEXCOORD5;
}; Tabla 22 - Estructura de salida
En este caso podemos observar la definición de la estructura de salida que usaremos en el
Vertex Shader. Esta estructura agrupa valores que serán necesarios en el Píxel Shader y que
saldrán interpolados para cada píxel.
Podemos observar como pasamos al Píxel Shader los valores de la posición en el mundo, al
igual como las coordenadas de textura y los valores de ‘h’ y ‘norm’ resultantes del cálculo
tangencial para posteriormente aplicar las texturas de Normal mapping.
Función del Vertex Shader
// -------------------------------------------------------------------
// Vertex Shader
// -------------------------------------------------------------------
VS_OUTPUT RenderSceneVS (
float4 inPos : POSITION,
float3 inNormal : NORMAL,
float2 inTex0 : TEXCOORD0,
float2 inTex1 : TEXCOORD1,
float3 inTangent : TEXCOORD2
)
{
VS_OUTPUT output;
output.pos = mul( float4(inPos.xyz,1.0f) , world_view_proj );
output.norm = float4(inNormal.xyz,1.0f);
output.tex0 = inTex0;
output.tex1 = inTex1;
float3 binormal = normalize(cross( inNormal , inTangent ));
float3x3 TBNMatrix = float3x3 ( normalize(inTangent) ,binormal,
normalize(inNormal));
output.norm = inPos;
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 86 de 134
float3 light_pos_to_px = light_pos - inPos;
float light_dist = length(light_pos_to_px);
float3 eye_pos_to_px = eye_pos - inPos;
float eye_dist = length(eye_pos_to_px);
float3 h = light_pos_to_px + (eye_pos_to_px * (light_dist /
eye_dist));
output.light_pos_to_px = mul( TBNMatrix ,light_pos_to_px);
output.h = mul( TBNMatrix ,h);
// Ponemos la pos local para que llegue al PS
output.pos_world = inPos;
return output;
}
Tabla 23 - Vertex Shader
En la tabla anterior podemos observar la función de Vertex Shader que tendrá nuestra
ecuación de iluminación. Esta función será ejecutada para cada uno de los vértices de la
escena y como tal, operará cada uno de estos vértices preparándolos para la función de Pixel
Shader.
Podemos observar como las coordenadas del vértice son multiplicadas por las matrices de
Mundo, Visión y Proyección combinadas entre ellas. Esto otorga al vértice la perspectiva
necesaria para ser representado en pantalla.
A continuación se realiza una de las operaciones más importantes en esta función. Y es el
cálculo de la matriz tangencial que nos permitirá más adelante poder calcular correctamente
las normales en los píxeles.
Esta matriz tangencial es calculada mediante tres vectores perpendiculares entre sí. Estos
vectores están compuestos por el vector de la tangente, dado por el vértice, el vector de la
normal, dado por el vértice, y el vector binormal, calculado a partir de los otros dos.
Una vez calculada esta matriz, solo nos queda hacer que los vectores que utilizaremos para la
iluminación sean locales al mundo descrito por la matriz tangencial.
A continuación veremos la función que sigue al Vertex Shader, esta función recibe el nombre
de Píxel Shader, y está encargada de calcular el color de cada uno de los píxeles que formaran
la imagen final.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 87 de 134
Función del Píxel Shader
// -------------------------------------------------------------------
// Pixel Shader
// -------------------------------------------------------------------
float4 RenderScenePS(
float2 inTex0 : TEXCOORD0,
float2 inTex1 : TEXCOORD1,
float3 pos_world : TEXCOORD2,
float3 light_pos_to_px : TEXCOORD3,
float3 h : TEXCOORD4,
float3 norm : TEXCOORD5
) : COLOR
{
inTex0.x = (inTex0.x * scalex) + scrollx;
inTex0.y = (inTex0.y * scaley) + scrolly;
//... Tabla 24 - Cabecera y coordenadas de textura del PS
En esta parte inicial de la función del Píxel Shader, podemos observar como entran los datos
en la función de igual forma que lo pueden hacer en el Vertex Shader. A continuación
observamos como las coordenadas de textura para este píxel son calculadas mediante las
coordenadas entrantes en la función modificadas por los parámetros que nos indica el material
desde fuera del shader.
Función del Píxel Shader
//...
float4 diffuse_texture = tex2D( DiffuseSampler , inTex0 );
float4 selfilu_texture = tex2D( SelfIlumSampler, inTex0 );
float4 lightmap_texture = tex2D( LightMapSampler , inTex1 );
float4 specular_texture = tex2D( SpecularSampler , inTex0 );
//... Tabla 25 - Sampleado de texturas
Podemos observar en la tabla anterior como las texturas son sampleadas. Esto quiere decir que a partir de las coordenadas de textura del píxel actual, podemos obtener el color que le corresponde según sus texturas asociadas. Este proceso lo realizamos para cada una de las componentes que formarán nuestra ecuación de iluminación.
Función del Píxel Shader
//...
light_pos_to_px = normalize( light_pos_to_px );
h = normalize( h );
//Obtenemos el color de la tinta de la luz
float4 cubemapInk_texture = texCUBE( CubeMapInkSampler, float4
(pos_world.xyz - light_pos.xyz,0.0f));
float shadow_dist = texCUBE( CubeMapSampler, float4
(pos_world.xyz - light_pos.xyz,1.0f)).x;
light_color = light_color * cubemapInk_texture;
//... Tabla 26 - Tintado de la luz
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 88 de 134
En esta última tabla, podemos observar el código con el que tintamos la luz. Un tinte de luz se
realiza mediante la multiplicación del color de la luz por el color obtenido en una textura
cubemap centrada en esta luz.
Función del Píxel Shader
//...
//Calculamos la distancia entre el pixel y el ojo de la camara
float dis_eye_pixel = distance(eye_pos,pos_world);
//Calculamos la distancia entre la luz y el pixel
float dis_light_pixel = distance(light_pos.xyz,pos_world.xyz);
float shadow_factor = (shadow_dist / dis_light_pixel);
if (dis_light_pixel < shadow_dist)
{
shadow_factor = 1.0f;
}
//... Tabla 27 - Distancias y sombreado en el PS
En la tabla anterior podemos observar cómo se calculan las distancias entre el ojo de la cámara
y el píxel, al mismo tiempo también podemos ver el cálculo de la distancia entre el píxel y la
luz.
Después de estos cálculos y accediendo a una textura de shadow cubemap, hacemos el cálculo
de la luz tal y como hemos descrito en el apartado teórico de las sombras.
Función del Píxel Shader
//...
float light_atenuation = saturate((light_pendent *
dis_light_pixel) + light_offset);
float4 light_factor = lerp(lightmap_texture, light_color,
light_atenuation);
float3 normal = tex2D( NormalSampler , inTex0 );
normal = 2 * normal - 1; //Pasamos de 0 a 1 -> -1 a 1
float diffuse = saturate( dot( normal , light_pos_to_px ) );
float cos_beta = saturate( dot( normal , h ) );
float specular_factor = saturate( pow( cos_beta , 30 ));
float4 specular = specular_factor * specular_texture *
light_atenuation;
float4 final = diffuse * ( (diffuse_texture + specular) *
light_factor * shadow_factor ) + ( selfilu_texture * glowintensity);
//... Tabla 28 - Ecuación de iluminación de PS
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 89 de 134
En la tabla anterior podemos observar el cálculo de la ecuación de iluminación propiamente
dicho. Este cálculo se realiza mediante una interpolación entre la luz dinámica y el color que
nos ofrece la textura de lightmap. Una vez calculadas las dos partes de la luz que hemos
comentado, estos valores son interpolados según las distancias de la luz.
Función del Píxel Shader
//...
//Calculo de la niebla
float f = saturate((fog_end - dis_eye_pixel)/(fog_end -
fog_start));
float4 color_fog = lerp(fog_color_far,fog_color_near,f);
float4 final_plus_fog = lerp(color_fog,final,f);
return ( final_plus_fog );
}
Tabla 29 - Cálculo de la niebla
Podemos observar en la tabla anterior, como una vez obtenido el color del píxel que
pondremos en pantalla, calculamos el color de la niebla para este píxel. Como ya hemos
comentado, esta niebla tiene dos colores, y el color interpolado de la niebla se vuelve a
interpolar con el color final del píxel.
Para terminar, mostramos la función del píxel shader sin subdivisiones de tablas para poder
tener una visión global de esta función.
Función del Píxel Shader
// -------------------------------------------------------------------
// Pixel Shader
// -------------------------------------------------------------------
float4 RenderScenePS(
float2 inTex0 : TEXCOORD0,
float2 inTex1 : TEXCOORD1,
float3 pos_world : TEXCOORD2,
float3 light_pos_to_px : TEXCOORD3,
float3 h : TEXCOORD4,
float3 norm : TEXCOORD5
) : COLOR
{
inTex0.x = (inTex0.x * scalex) + scrollx;
inTex0.y = (inTex0.y * scaley) + scrolly;
float4 diffuse_texture = tex2D( DiffuseSampler , inTex0 );
float4 selfilu_texture = tex2D( SelfIlumSampler, inTex0 );
float4 lightmap_texture = tex2D( LightMapSampler , inTex1 );
float4 specular_texture = tex2D( SpecularSampler , inTex0 );
light_pos_to_px = normalize( light_pos_to_px );
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 90 de 134
h = normalize( h );
//Obtenemos el color de la tinta de la luz
float4 cubemapInk_texture = texCUBE( CubeMapInkSampler, float4
(pos_world.xyz - light_pos.xyz,0.0f));
float shadow_dist = texCUBE( CubeMapSampler, float4
(pos_world.xyz - light_pos.xyz,1.0f)).x;
light_color = light_color * cubemapInk_texture;
//Calculamos la distancia entre el pixel y el ojo de la camara
float dis_eye_pixel = distance(eye_pos,pos_world);
//Calculamos la distancia entre la luz y el pixel
float dis_light_pixel = distance(light_pos.xyz,pos_world.xyz);
float shadow_factor = (shadow_dist / dis_light_pixel);
if (dis_light_pixel < shadow_dist)
{
shadow_factor = 1.0f;
}
float light_atenuation = saturate((light_pendent *
dis_light_pixel) + light_offset);
float4 light_factor = lerp(lightmap_texture, light_color,
light_atenuation);
float3 normal = tex2D( NormalSampler , inTex0 );
normal = 2 * normal - 1; //Pasamos de 0 a 1 -> -1 a 1
float diffuse = saturate( dot( normal , light_pos_to_px ) );
float cos_beta = saturate( dot( normal , h ) );
float specular_factor = saturate( pow( cos_beta , 30 ));
float4 specular = specular_factor * specular_texture *
light_atenuation;
float4 final = diffuse * ( (diffuse_texture + specular) *
light_factor * shadow_factor ) + ( selfilu_texture * glowintensity);
//Calculo de la niebla
float f = saturate((fog_end - dis_eye_pixel)/(fog_end -
fog_start));
float4 color_fog = lerp(fog_color_far,fog_color_near,f);
float4 final_plus_fog = lerp(color_fog,final,f);
return ( final_plus_fog );
} Tabla 30 - Función del PS sin subdivisiones
En esta última tabla observamos el código responsable de la ecuación de iluminación de
nuestro motor 3D. Hemos de tener en cuenta que este código se ejecuta por cada píxel de un
frame, o lo que es lo mismo, estamos viendo indirectamente la potencia que tienen las tarjetas
gráficas de hoy en día para poder hacer todos estos cálculos en el tiempo requerido.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 91 de 134
3.2.2 Luces
Como hemos comentado en el apartado teórico, las luces de este motor tienen diferentes parámetros. Parámetros que nos indican hasta donde llega la luz, el color de ésta, etc. A continuación veremos como estas luces han sido implementadas en nuestro motor.
Inicialización de la luz
//--------------------------------------------------------------------
// USE: Constructor de la clase
// IN: Void
// OUT: Void
//--------------------------------------------------------------------
CLight::CLight()
{
m_cLightType = IL_LIGHT_OMNI;
m_fAngle = 90.0f;
m_fPendent = -1.0f;
m_fOffset = 1.0f;
m_vPosition.x = 0.0f; m_vPosition.y = 0.0f;
m_vPosition.z = 0.0f; m_vPosition.w = 1.0f;
m_vDirection.x = 0.0f; m_vDirection.y = 0.0f;
m_vDirection.z = 0.0f; m_vDirection.w = 1.0f;
}
Tabla 31 - Constructor de la Luz
Empezamos con el constructor de la luz, que nos indica cómo serán inicializadas las luces en su
inicio. Como podemos observar, todas las luces en un inicio serán de tipo omnidireccional con
un ángulo de 90 grados que no será usado, y con un pendiente y offset en las distancias de la
luz de -1 y 1.
Al mismo tiempo también podemos observar como los valores de posición de la luz y dirección
de ésta están expresados mediante un vector de cuatro componentes. Tenemos que tener en
cuenta de que esto es así debido a que los shaders acostumbran a recibir vectores de cuatro
componentes, aunque nosotros de esas cuatro componentes solo usaremos tres.
Entrada de la posición de la luz
//--------------------------------------------------------------------
// USE: Introduce la posicion de la luz
// IN: D3DXVECTOR3& - Posicion de la luz (x,y,z,w)
// OUT: Void
//--------------------------------------------------------------------
void CLight::SetPosition(D3DXVECTOR4& inVec)
{
m_vPosition = inVec;
}
Tabla 32 - Posición de la Luz
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 92 de 134
En la tabla anterior podemos observar lo que estábamos comentando de las cuatro
componentes de los vectores. Podemos observar como la posición 3D de la luz esta descrita
por un vector de cuatro componentes, de las cuales, la última (w) será ignorada.
Ilustración 45 - Diagrama matemático de la Luz
Hemos recuperado el diagrama de la luz que teníamos en el apartado teórico. En este
diagrama hemos substituido y añadido valores matemáticos los cuales ahora podemos
entender mejor.
La atenuación de la luz se genera mediante una ecuación de una recta, nosotros le pasamos al
motor los radios (interior y exterior) y este genera los valores m y n de nuestra ecuación.
En la tabla siguiente podremos observar cómo se realiza ese proceso a nivel de código.
Luz Omni
Radio exterior
Radio interior
Y = -mX + n
Y+
X+
+1
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 93 de 134
Entrada de los Radios de la Luz
//--------------------------------------------------------------------
// USE: Introduce los radios de la uz
// IN: float,float - Radio interior, Radio exterior
// OUT: Void
//--------------------------------------------------------------------
void CLight::SetLightRadis(float inRadiNear, float inRadiFar, float
inTime)
{
//Comprobamos que los valores de los radios no sean negativos
if(inRadiNear < 0.0f) inRadiNear = 0.0f;
if(inRadiFar < 0.0f) inRadiFar = 0.0f;
//Si el radio externo es menor que el interno, pasan a valer lo
//mismo
if(inRadiFar <= inRadiNear)
{
inRadiFar = inRadiNear;
}
//Si los radios son iguales, el radio exterior valdra una unidad
//mas
if(inRadiNear == inRadiFar)
{
inRadiFar = inRadiNear + 1.0f;
}
m_Radis.SetNewData(D3DXVECTOR4(inRadiNear,inRadiFar,0.0f,1.0f),i
nTime);
if(inTime > 0.0f)
{
}
else
{
m_fPendent = -1.0f / (inRadiFar - inRadiNear);
m_fOffset = m_fPendent * inRadiFar * -1.0f;
}
}
Tabla 33 - Generación de los parámetros m y n de la recta
En la tabla anterior podemos observar como a partir de dos radios, uno interno y otro externo,
generamos una serie de comparaciones para que estos radios sean correctos. Que un radio sea
correcto indica entre otras cosas que el radio externo no sea menor que el radio interno, o que
los dos radios no tengan valor cero a la vez.
Finalmente podemos observar como a partir de esos radios, se generan los parámetros de
Pendiente y Offset. Estos valores corresponden a los valores de m y n en la ecuación de la
recta.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 94 de 134
A continuación comentaremos como las luces cambian de parámetros de forma fluida y no
bruscamente.
Actualización de la Luz
//--------------------------------------------------------------------
// USE: Actualiza el color y los radios de la luz segun el tiempo
// IN: float - Elapsed Time
// OUT: Void
//--------------------------------------------------------------------
void CLight::Update(float inElapsedTime)
{
m_Color.Update(inElapsedTime);
m_Radis.Update(inElapsedTime);
if(m_Radis.GetTime() > 0.0f)
{
m_fPendent = -1.0f / (m_Radis.GetOutData().y -
m_Radis.GetOutData().x);
m_fOffset = m_fPendent * m_Radis.GetOutData().y * -1.0f;
}
}
Tabla 34 - Actualización de los parámetros de la Luz
En la tabla anterior podemos observar como los parámetros de la luz son actualizados. Esta
actualización se lleva a cabo mediante un objeto interno del motor 3D creado para tal efecto y
denominado ‘Interpolator’.
El ‘Interpolator’ es un vector de cuatro componentes que interpola entre dos valores gracias a
una diferencia de tiempo que nosotros le proporcionamos.
Podemos observar como en la tabla son actualizados el color y los radios de la luz, y si la
diferencia de tiempo para los radios aún está activa, entonces actualizamos los parámetros de
Pendiente y Offset de la ecuación de la recta de la luz.
3.2.3 Texturas
El objeto de textura ha sido creado para encapsular el tipo básico de textura relacionado
directamente con la librería gráfica DirectX.
De este modo aseguramos una mayor portabilidad a la hora de cambiar de librería pues este
objeto es el único que cambiará y creará una frontera para que los restantes objetos que
llamen a éste no tengan que ser cambiados.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 95 de 134
Como podemos observar a continuación, esta es la clase que engloba una textura de DirectX
en nuestro motor 3D.
Declaración de la textura
#include "DXUT.h"
class CTexture
{
private:
unsigned int m_iId;
unsigned int m_iInstances;
LPDIRECT3DTEXTURE9 m_tTexture;
//...
}
Tabla 35 - Encapsulado de Textura
Como vemos en la tabla anterior, el tipo de texturas en DirectX es LPDIRECT3DTEXTURE9 y
queda encapsulado en esta clase, creando la frontera que hemos comentado antes para que
no haya más cambios por encima de este objeto.
Para realizar este encapsulado, aparte de declarar un objeto de tipo textura, también
necesitamos unas funciones miembro que nos aseguren una correcta gestión de las texturas
en nuestro motor 3D.
A continuación veremos estas funciones.
Funciones miembro de la textura
public:
CTexture ();
CTexture (const CTexture& inObj);
~CTexture ();
void SetId (unsigned int inId);
void SetInstances (unsigned int inInst);
void SetTexturePtr (LPDIRECT3DTEXTURE9 inPtr);
void IncInstances ();
unsigned int GetId () const;
unsigned int GetInstances () const;
LPDIRECT3DTEXTURE9 GetTexturePtr () const;
void Release ();
CTexture& operator= (const CTexture& inObj);
bool operator== (const CTexture& inObj);
bool operator< (const CTexture& inObj);
Tabla 36 - Cuerpo de la clase textura
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 96 de 134
En la tabla anterior podemos observar el cuerpo de la clase textura, la cual nos ofrece
sistemas de gestión como las funciones para incrementar instáncias o decrementar, para
poder asegurar una copia única de texturas en memoria y así ser más eficientes con la
memoria de la tarjeta gráfica.
A nivel del manager de texturas, este nos ofrece unas funciones de gestión un poco diferentes
a las vistas en la clase textura.
Funciones miembro de manager de textura
class CTextureMgr
{
private:
vector<CTexture> m_vTextures;
vector< pair< wstring, unsigned int> > m_vNamesId;
wstring m_sRelativePath;
LPDIRECT3DDEVICE9 m_pDevice;
public:
CTextureMgr ();
~CTextureMgr ();
void SetRelativePath (const char* inPath);
void SetDevicePtr (LPDIRECT3DDEVICE9 inPtr);
unsigned int LoadTexture (const char* inName);
const CTexture& GetTexture (unsigned int inIndex)const;
bool ReleaseTexture (unsigned int inId, unsigned
int *outId);
void Release ();
};
Tabla 37 - Manager de textura
Como podemos observar en la tabla anterior, el manager de textura usa un camino de archivo
(path) relativo para así poder acceder a todas las texturas únicamente por su nombre y no
tener que poner todo el camino completo cada vez.
De igual forma, nos ofrece funciones para cargar y descargar las texturas a voluntad y en
tiempo real, esto nos asegura que las texturas cargadas en memoria son las óptimas si
optamos por cargarlas a medida que las necesitemos.
Por último comentar que el manager de texturas necesita inequívocamente el puntero al
objeto device de la librería DirectX para poder trabajar correctamente.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 97 de 134
3.2.4 Materiales
Como hemos visto en la sección teórica, los materiales nos sirven para poder dotar a los
objetos de propiedades visuales complejas sin necesidad de mucho esfuerzo.
Cada uno de los materiales está considerado como un objeto en memoria al igual como una
textura. Para un material, tenemos definido una clase que actuará como objeto contenedor de
la información de un material individual, también tenemos definido un formato XML para
poder representar ese material en disco.
A continuación veremos cómo ha estado definida la clase contenedora de un material y cuáles
son sus funciones asociadas.
Funciones de gestión de un Material
class CMaterial
{
private:
unsigned int m_iId;
unsigned int m_iDiffuseId;
//...
float m_fGlowIntensity;
string m_sTechName;
public:
CMaterial ();
CMaterial (const CMaterial& inObj);
~CMaterial ();
void SetId (unsigned int inId);
void SetEffectId (unsigned int inEId);
void SetInstances (unsigned int inInst);
void SetFlags (unsigned char inFlags);
void SetDiffuseId (unsigned int inId);
void SetNormalId (unsigned int inId);
void SetSpecularId (unsigned int inId);
//...
unsigned int GetEffectId () const;
unsigned int GetInstances () const;
unsigned char GetFlags () const;
unsigned int GetDiffuseId () const;
unsigned int GetNormalId () const;
unsigned int GetSpecularId () const;
CMaterial& operator= (const CMaterial& inObj);
bool operator== (const CMaterial& inObj);
bool operator< (const CMaterial& inObj);
};
Tabla 38 - Clase Material
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 98 de 134
Como podemos observar en la tabla anterior, el material como objeto está dotado de
múltiples funciones de gestión que no han sido representadas en su totalidad. Podemos
observar funciones para la inserción de identificadores de textura, necesarios para poder
acceder a las texturas que necesitemos.
Por otro lado también podemos ver cómo están definidos los operadores de igualdad,
comparación de igualdad y comparación de más grande que. Estos operadores nos ayudan a
que el código escrito sea más portable y rápido para el desarrollador.
A continuación mostramos el formato de un material escrito en disco, para así poder dar más
flexibilidad al motor 3D y no necesitar recompilarlo cada vez que haya un cambio.
Definición de un material
<Material name="Tronco">
<Properties>
<Scroll x="0.0" y="0.0"/>
<Scale x="3.0" y="5.0"/>
<Shader file="planar.fx" relname="planar.fx" technique="Basic"/>
</Properties>
<Textures>
<Diffuse name="Tronco1_d.tga"/>
<Normal name="Tronco1_n.tga"/>
<Specular name="default_g.tga"/>
<SelfIlum name="default_g.tga" intensity="0.2"/>
<LightMap name="default_l.tga"/>
</Textures>
</Material>
Tabla 39 - Formato de material
En la tabla anterior vemos el formato, que nos indica en cada momento cuales son las texturas
asociadas a cada uno de los tipos de texturas. Podemos contar cinco tipos diferentes como
comentamos en la sección teórica.
Fijémonos en el hecho de que cuando no queremos una textura en concreto para un canal,
esta debe ser definida como una textura por defecto. Esto nos asegura que no habrá ninguna
ambigüedad en el material y este se comportara de la manera deseada.
Al mismo tiempo también vemos como los parámetros de escalado y deslizamiento están
definidos. Estos parámetros pueden tener sus valores por defecto, o por el contrario, pueden
tener valores para que el material en pantalla sufra cambios de aspecto.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 99 de 134
Cada uno de estos materiales, es almacenado en un manager de materiales, el cual gestionara
cada uno de ellos y nos asegurará entre otras cosas que no haya materiales repetidos.
A continuación mostraremos las funciones de un manager de materiales para observarlas con
mayor precisión.
Definición del manager de materiales
class CMaterialMgr
{
private:
vector<CMaterial> m_vMaterials;
vector<unsigned int> m_vDeleted;
vector<unsigned int> m_vRelations;
vector< pair< wstring, unsigned int> > m_vNamesId;
CTextureMgr *m_pTexMgr;
CEffectMgr *m_pEffMgr;
string m_sRelativePath;
protected:
bool IsMatDeleted (unsigned int inIndex);
void UndeleteMat (unsigned int inIndex);
void ChangeTextIds (unsigned int inId1, unsigned int
inId2);
public:
CMaterialMgr ();
CMaterialMgr (const CMaterial& inObj);
~CMaterialMgr ();
void SetRelativePath (const char* inPath);
void SetTexMgrPtr (CTextureMgr *inPtr);
void SetEffMgrPtr (CEffectMgr *inPtr);
unsigned int SetMaterial (CMaterial& inMat);
unsigned int LoadMaterial (const char* inName);
CMaterial& GetMaterial (unsigned int inIndex);
void Release ();
void ReleaseMaterial (unsigned int inIndex);
};
Tabla 40 - Clase del manager de materiales
Como podemos observar en la tabla anterior, el manager de materiales está formado
internamente por unas estructuras de vectores mediante las que se relacionan los nombres de
los materiales con estos mismos.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 100 de 134
Por otro lado tenemos que el manager de materiales funciona prácticamente igual que la
metodología que hemos visto en el manager de texturas, cargando y descargando un material
cuando nosotros necesitemos.
3.2.5 Render Targets
Los Render Targets nos sirven para crear nuevas superficies de renderizado tal y como lo
explicamos en el apartado teórico correspondiente.
Cada uno de estos Render Targets será creado como objeto en nuestro motor 3D. Para cada
uno de ellos se definirá sus dimensiones y tipo de píxel. Gracias a esto podremos usar cada uno
de los Render Targets con el fin que queramos.
A continuación mostramos la clase que engloba los Render Targets y así tenemos una mejor
idea de cómo son accedidos por el motor 3D.
Definición del Render Target
class CRenderTarget
{
private:
LPD3DXRENDERTOSURFACE m_RenderToSurface;
LPDIRECT3DSURFACE9 m_Surface;
D3DFORMAT m_ColorFormat;
D3DFORMAT m_StencilZBufferFormat;
LPDIRECT3DTEXTURE9 m_tTexture;
LPDIRECT3DDEVICE9 m_Device;
bool m_bUseDepthBuffer;
public:
CRenderTarget ();
~CRenderTarget ();
bool CreateTexture ( LPDIRECT3DDEVICE9
inDev, unsigned int inWidth, unsigned int inHeight, D3DFORMAT
inColorFormat = D3DFMT_A8R8G8B8, D3DFORMAT inStencilFormat =
D3DFMT_UNKNOWN);
void BeginScene ();
void EndScene ();
void SaveToFile (const char* inName);
void DumpSurface (unsigned int inPosX1,
unsigned int inPosY1,unsigned int inOffX2,unsigned int inOffY2);
void LoadFromBackBuffer (unsigned int inPosX1,
unsigned int inPosY1,unsigned int inOffX2,unsigned int inOffY2);
void Release ();
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 101 de 134
LPDIRECT3DTEXTURE9 GetTexture ();
LPDIRECT3DSURFACE9 GetSurface ();
};
Tabla 41 - Clase Render Target
Fijémonos en que los datos internos del Render Target incluyen tanto una
LPDIRECT3DSURFACE9 como un LPDIRECT3DTEXTURE9. Esto es así debido a que el proceso
de render necesita una superficie para renderizar, pero el motor 3D necesita una textura para
poder operar con ella. De este modo tenemos una textura relacionada con su superficie y todo
encapsulado en una misma clase.
En la interfaz de la clase, podemos observar como la función para crear un Render Target
recibe numerosos parámetros que hemos comentado anteriormente. También podemos
observar que existen funciones para hacer volcados de memoria gráfica, tanto de Render
Target al Back Buffer como a la inversa.
Por último comentar que también existen las funciones para acceder desde fuera a la textura y
superficie que comentábamos antes.
A continuación veremos cómo se inicializa un Render Target.
Cómo se inicializa un Render Target
//--------------------------------------------------------------------
// USE: Crea la textura en memoria donde sera renderizada la escena
// deseada
// IN: Deivce, ancho y alto, y formatos tanto de color como de
// Stencil buffer
// OUT: bool - Cierto si ha ido bien, falso si hay algun error
//--------------------------------------------------------------------
bool CRenderTarget::CreateTexture( LPDIRECT3DDEVICE9 inDev, unsigned
int inWidth, unsigned int inHeight, D3DFORMAT inColorFormat, D3DFORMAT
inStencilFormat)
{
// Salvamos el device y demas para posibles usos
m_Device = inDev; //d3d_device = ad3d_device;
m_ColorFormat = inColorFormat; //color_format = acolor_format;
m_StencilZBufferFormat = inStencilFormat;
HRESULT hr;
hr = D3DXCreateTexture( m_Device,
inWidth, //Hanchura
inHeight, //Altura
1, //mip levels
D3DUSAGE_RENDERTARGET,
m_ColorFormat,
D3DPOOL_DEFAULT,
&m_tTexture);
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 102 de 134
if(hr != D3D_OK)
{
return (false);
}
hr = m_tTexture->GetSurfaceLevel(0, &m_Surface);
assert (hr == D3D_OK);
D3DSURFACE_DESC desc;
hr = m_Surface->GetDesc(&desc);
assert (hr == D3D_OK);
m_bUseDepthBuffer =( m_StencilZBufferFormat != D3DFMT_UNKNOWN );
hr = D3DXCreateRenderToSurface( m_Device,
desc.Width,
desc.Height,
desc.Format,
m_bUseDepthBuffer,
m_StencilZBufferFormat,
&m_RenderToSurface
);
return (hr == D3D_OK);
}
Tabla 42 - Inicialización de un Render Target
En esta tabla podemos observar como la textura y la superficie asociada al Render Target están
relacionadas entre sí, para que tanto el motor 3D como el proceso de render se puedan
beneficiar de ello.
3.2.6 Efectos Especiales
En lo que a efectos especiales se refiere, veremos conjuntamente como se hace la niebla y
como se hace el efecto Glow.
En la sección teórica vimos sus principios teóricos y como deberían ser encarados. Ahora,
veremos más en detalle como son realizados estos efectos especiales.
Hay que comentar primero que estos dos efectos especiales son realizados mediante las
instrucciones de píxel shader, y que por eso los trataremos juntos ya que están relacionados
por el proceso que los trata a la hora de hacer el Render.
Como primer efecto especial, veremos la niebla. Esta niebla está gobernada por un objeto que
controla en todo momento sus parámetros para pasárselos al shader correspondiente que nos
hará el render final.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 103 de 134
Definición de la niebla
class CFogMgr
{
private:
unsigned int m_iFramesRemainDis;
unsigned int m_iFramesRemainCol;
float m_fStart;
float m_fEnd;
float m_fIncStart;
float m_fIncEnd;
D3DXVECTOR4 m_FogColorNear;
D3DXVECTOR4 m_FogColorFar;
D3DXVECTOR4 m_IncColorNear;
D3DXVECTOR4 m_IncColorFar;
CInterpolator m_ColorNear;
CInterpolator m_ColorFar;
CInterpolator m_FogDistances;
public:
CFogMgr ();
~CFogMgr ();
void SetFogDistance (float inStart, float inEnd,
float inTime);
void SetFogColor (D3DXVECTOR4& inColNear,
D3DXVECTOR4& inColFar, float inTime);
void Update (float inElapsedTime);
float GetStart () const;
float GetEnd () const;
const D3DXVECTOR4& GetFogColorNear () const;
const D3DXVECTOR4& GetFogColorFar () const;
};
Tabla 43 - Clase de la niebla
Como podemos observar en la tabla anterior, la niebla está formada por 3 objetos de tipo
‘interpolator’. Estos objetos como ya hemos hablado anteriormente de ellos, son capaces de
darnos un valor interpolado entre dos valores predefinidos por el programador.
Al igual que necesitamos informar a la niebla de los colores que debe tener, también tenemos
que hacer una cosa muy importante y es actualizarla a través del tiempo. Gracias a la función
de actualización presente en esta clase, la niebla puede, mediante un tiempo determinado,
hacer los cambios pertinentes entre un color actual, y otro color futuro.
Fijémonos a continuación en el interior de la función de actualización de la niebla y podremos
observar como esta procede a través del tiempo.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 104 de 134
Actualización de la niebla
//--------------------------------------------------------------------
// USE: Actualiza segun los frames el color de la niebla
// IN: void
// OUT: Void
//--------------------------------------------------------------------
void CFogMgr::Update(float inElapsedTime)
{
m_ColorNear.Update(inElapsedTime);
m_ColorFar.Update(inElapsedTime);
m_FogDistances.Update(inElapsedTime);
}
Tabla 44 - Función de actualización de la niebla
Como podemos observar en la tabla anterior, los objetos ‘interpolator’ de la niebla son
actualizados mediante una porción de tiempo. Estos objetos en su interior se actualizan para
que siempre que nosotros pidamos sus valores, estos nos los devuelvan correctamente.
Una vez tenemos los valores que nos devuelve la niebla según sus parámetros actualizados a
través del tiempo, solo nos falta introducir dichos datos en el shader que renderiza la escena.
A continuación veremos las instrucciones que nos crean la niebla en pantalla.
Instrucciones de la niebla en el Shader
float f = saturate((fog_end - dis_eye_pixel)/(fog_end - fog_start));
float4 color_fog = lerp(fog_color_far,fog_color_near,f);
float4 final_plus_fog = lerp(color_fog,final,f);
Tabla 45 - Código de renderizado de la niebla
En la tabla anterior podemos observar como el factor de la niebla es calculado a partir de las
distancias de ésta y como acto seguido, son interpolados los colores de la niebla con el color
real del píxel de la escena.
Como ya se comentó en el apartado teórico, el hecho de hacer la niebla a partir de dos colores
es un aspecto experimental que ha dado unos buenos resultados a la hora de la verdad
añadiendo solo una instrucción más al píxel shader.
A continuación, trataremos el siguiente efecto especial que denominamos Glow. Si
recordamos lo que explicamos en el apartado teórico, el Glow es una representación de un
material que por sí sólo pretender emanar luz a la escena.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 105 de 134
Este efecto se consigue mediante una textura donde se especifica el lugar y el color del efecto
seguido por un proceso de post procesado que nos crea el efecto deseado.
La metodología para aplicar el Glow consiste en un efecto de post procesado de emborronar la
información de color que nos da la textura de Glow.
A continuación podemos ver el código que nos permite hacer este efecto.
Post procesado del Glow
for(unsigned int i = 0; i < m_iGlowLoops; i++)
{
m_RT2.BeginScene();
// Limpiamos el RenderTarget para pintar las texturas de
SelfIlumination
V( m_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET |
D3DCLEAR_ZBUFFER, D3DCOLOR_ARGB(0, 0, 0, 0), 1.0f, 0) );
unsigned int cPasses;
m_EffectMgr.GetEffect(IL_EFFECT_GLOW)->Begin( &cPasses, 0 );
//...
for( unsigned int p = 0; p < cPasses; ++p )
{
m_EffectMgr.GetEffect(IL_EFFECT_GLOW)->BeginPass( p );
m_pD3DDevice->SetFVF( D3DFVF_TEXTUREDVERTEX );
m_pD3DDevice->DrawPrimitiveUP( D3DPT_TRIANGLESTRIP, 2,
m_vRenderQuad, sizeof( TEXTUREDVERTEX ) );
m_EffectMgr.GetEffect(IL_EFFECT_GLOW)->EndPass();
}
m_EffectMgr.GetEffect(IL_EFFECT_GLOW)->End();
m_RT2.EndScene();
m_RT3.BeginScene();
// Limpiamos el RenderTarget para pintar las texturas de
SelfIlumination
V( m_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET |
D3DCLEAR_ZBUFFER, D3DCOLOR_ARGB(0, 0, 0, 0), 1.0f, 0) );
//...
for( unsigned int p = 0; p < cPasses; ++p )
{
m_EffectMgr.GetEffect(IL_EFFECT_GLOW)->BeginPass( p );
m_pD3DDevice->SetFVF( D3DFVF_TEXTUREDVERTEX );
m_pD3DDevice->DrawPrimitiveUP( D3DPT_TRIANGLESTRIP, 2,
m_vRenderQuad, sizeof( TEXTUREDVERTEX ) );
m_EffectMgr.GetEffect(IL_EFFECT_GLOW)->EndPass();
}
m_EffectMgr.GetEffect(IL_EFFECT_GLOW)->End();
m_RT3.EndScene();
}
Tabla 46 - Metodología del Glow
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 106 de 134
En la tabla anterior podemos observar que la metodología del Glow se basa en una serie de
bucles que emborronan los píxeles de la imagen para poder formar el efecto deseado. El
número de vueltas que se aplican a este efecto es un parámetro proporcionado por el
desarrollador
Post procesado de Glow en el Píxel Shader
float4 RenderGlowPS(
float2 inTex0 : TEXCOORD0
) : COLOR
{
float2 pixelSize = float2 (dx, dy);
float2 texCoordSample = 0;
float2 halfPixelSize = pixelSize / 2.0f;
float2 dUV = (pixelSize.xy*float(iteration))+ halfPixelSize.xy;
float3 cOut;
//Recuperamos el pixel de arriba-izquierda
texCoordSample.x = inTex0.x - dUV.x ;
texCoordSample.y = inTex0.y + dUV.y;
cOut = tex2D(GlowSampler, texCoordSample);
//Recuperamos el pixel de arriba-derecha
texCoordSample.x = inTex0.x + dUV.x;
texCoordSample.y = inTex0.y + dUV.y;
cOut += tex2D(GlowSampler, texCoordSample);
//Recuperamos el pixel de abajo-derecha
texCoordSample.x = inTex0.x + dUV.x;
texCoordSample.y = inTex0.y - dUV.y;
cOut += tex2D(GlowSampler, texCoordSample);
//Recuperamos el pixel de abajo-izquierda
texCoordSample.x = inTex0.x - dUV.x;
texCoordSample.y = inTex0.y - dUV.y;
cOut += tex2D(GlowSampler, texCoordSample);
cOut*= 0.25f;
return (float4(cOut,1.0f));
}
Tabla 47 - Píxel Shader del post procesado Glow
En la tabla anterior podemos observar como el código del píxel shader encargado de hacer el
efecto Glow accede a los píxeles vecinos de estos y hace una media para así poder devolver
ese valor como resultado.
Este proceso nos da como resultado un emborronamiento de la imagen que será necesario
debido a la naturaleza del efecto Glow.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 107 de 134
3.2.7 Estructura Pipe-Line
En el apartado teórico hablamos sobre la estructura pipeline que tiene nuestro motor 3D para
generar cada una de las imágenes que nos muestra por pantalla.
La estructura pipeline no es más que la secuencia de procesos que debemos realizar para
poder construir cada unos de los fotogramas que mostraremos en pantalla.
Inicializamos las matrices
//Recuperamos las matrices necesarias
mWorld = mIdentity = *D3DXMatrixIdentity(&mWorld);
mProj = m_Camera.GetProjMatrix();
mView = m_Camera.GetViewMatrix();
mWorldViewProjection = mWorld * mView * mProj;
//Actualizamos la posicion de la luz
vLightPos.x = m_Camera.GetTarget().x;
vLightPos.y = m_Camera.GetTarget().y;
vLightPos.z = m_Camera.GetTarget().z;
vLightPos.w = 1.0f;
m_vLights[0].SetPosition(vLightPos);
Tabla 48 - Inicialización de matrices
Como podemos observar en la última tabla, el primer proceso que debemos realizar es la
actualización de las matrices involucradas en la generación del fotograma actual.
Por ello será necesario recuperar estas matrices de la cámara activa que tengamos en estos
momentos.
Inicializamos la geometría
// Cargamos solo aquella geometria que sea visible por la camara
LoadVisibleMesh(mWorldViewProjection);
//Cargamos la geometria visible para cada una de las caras del
//cubemap
LoadVisibleMeshCubeMap();
//Ordenamos la geometria segun el material que tienen.
SortVisibleMesh();
//Renderizamos las caras del cubemap
RenderShadowCubeMap();
Tabla 49 - Inicialización de la geometría
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 108 de 134
Junto a esta última tabla y si recordamos el diagrama de la estructura pipeline en el apartado
teórico, podremos relacionar los procesos de la inicialización de la geometría, al igual como la
ordenación de ésta y la generación de los datos necesarios para hacer las sombras.
Podemos observar como la inicialización de la geometría para utilizar solo aquella que estamos
viendo en estos momentos se hace a partir de la matriz resultante de la cámara activa en estos
instantes.
Momentos después se procede a ordenar esta geometría y a generar los shadowmap para su
utilización en el sombreado.
Primeros pasos para hacer el Glow
//Renderizamos los objetos que tienene SelfIlum en un
//rendertarget
if(m_bDebugGlow)
{
SetQuadUVtoRT(m_iScreenWidth,m_iScreenHeight);
RenderSelfIlum(&mWorldViewProjection);
}
else
{
fGlowActive = 0.0f;
}
RenderSkyBox(&mWorldViewProjection);
Tabla 50 - Inicialización del Glow
En esta última tabla, podemos observar cómo se hacen los preparativos para hacer el Glow en
la escena.
Podemos ver cómo si el Glow está activo, se renderiza la geometría con el material específico
de Glow activado.
Poco después de hacer este proceso, procedemos a renderizar lo que en la escena será el cielo
o cubo envolvente.
Renderizamos en el Back Buffer
if( SUCCEEDED( m_pD3DDevice->BeginScene() ) )
{
Tabla 51 - Activación del Back Buffer
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 109 de 134
A continuación observamos una de las partes importantes de nuestro proceso de renderizado
y es el hecho de renderizar la geometría principal en el Back Buffer y no en un Render Target.
Esto es así debido a que si renderizamos en un Render Target, perderemos la habilidad que
nos ofrece la librería gráfica para aplicar vía hardware un filtrado de Anti-Aliasing en dicha
geometría.
Paso de parámetros al shader
V(m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->SetMatrix("world_view_proj",
&mWorldViewProjection ) );
V(m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->SetVector("light_pos",
&vLightPos ) );
V(m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->SetVector("light_color",
&m_vLights[0].GetColor() ) );
V(m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->SetFloat("light_pendent",
m_vLights[0].GetPendent() ) );
V(m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->SetFloat("light_offset",
m_vLights[0].GetOffset() ) );
V(m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->SetVector("eye_pos",
&vEyePos ) );
V(m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->SetFloat("fog_start",
m_FogMgr.GetStart() ) );
V(m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->SetFloat("fog_end",
m_FogMgr.GetEnd() ) );
V(m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->SetVector("fog_color_near",
&m_FogMgr.GetFogColorNear() ) );
V(m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->SetVector("fog_color_far",
&m_FogMgr.GetFogColorFar() ) );
Tabla 52 - Inicialización de los parámetros del shader
En la tabla anterior podemos observar cómo pasamos los parámetros necesarios al shader
para que éste pueda renderizar adecuadamente la geometría que nosotros le indiquemos.
Uno de los parámetros más importantes que debemos pasarle al shader es la matriz resultante
de la cámara para que la perspectiva pueda ser creada acorde con nuestras necesidades.
Activación del modo alambre
//Si esta activado el bool de renderizado en alambre, entonces
//activamos el flag interno del pipeline para poder pintar de esta
//forma
if(m_bWireMode)
{
m_pD3DDevice->SetRenderState(D3DRS_FILLMODE,D3DFILL_WIREFRAME);
}
Tabla 53 - Parámetro para pintado en alambre
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 110 de 134
Como podemos observar en esta última tabla, con ese parámetro podemos activar la función
de pintado mediante alambre de la geometría. Esta función es puramente un método de
depuración o metodología usada mientras se desarrolla la aplicación, ya que en pleno uso, la
aplicación no mostrará este tipo de pintado.
Renderizado de la geometría principal
for(unsigned int j = 0; j < (unsigned int)m_vSubMeshes.size(); j++)
{
unsigned int VIndex;
unsigned int IIndex;
iMaterial=m_SSubMeshMgr.GetSSubMesh(m_vSubMeshes[j]).GetMatId();
m_MaterialMgr.GetMaterial(iMaterial).Update(inElapsedTime);
if(m_MaterialMgr.GetMaterial(iMaterial).GetGlowIntensity()>0.5f)
{
int a = 1;
}
m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->SetTechnique(
m_MaterialMgr.GetMaterial(iMaterial).GetTechName() );
V( m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->SetFloat( "scalex",
m_MaterialMgr.GetMaterial(iMaterial).GetScalex() ) );
//...
unsigned int cPasses;
m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->Begin( &cPasses, 0 );
for( unsigned int p = 0; p < cPasses; ++p )
{
m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->BeginPass( p );
//Para cada sub-mesh, ponemos su material en posicion para
ser utilizado
handle_param = m_EffectMgr.GetEffect(IL_EFFECT_BASIC)-
>GetParameterByName( NULL, "DiffuseTexture" );
//...
m_pD3DDevice->SetFVF(FVF_Format);
m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->CommitChanges();
VIndex = m_SSubMeshMgr.GetSSubMesh(m_vSubMeshes[j]).GetVBId();
IIndex = m_SSubMeshMgr.GetSSubMesh(m_vSubMeshes[j]).GetIBId();
m_pD3DDevice->DrawIndexedPrimitiveUP( D3DPT_TRIANGLELIST,
0,
m_VertexArrayMgr.GetVertexArray(VIndex).GetNumVertex(),
m_IndexArrayMgr.GetIndexArray(IIndex).GetNumIndex(),
m_IndexArrayMgr.GetIndexArray(IIndex).GetIndexPtr(),
D3DFMT_INDEX16,
m_VertexArrayMgr.GetVertexArray(VIndex).GetVertexPtr(),
sizeof(FVFVertex)
);
m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->EndPass();
}
m_EffectMgr.GetEffect(IL_EFFECT_BASIC)->End();
}
Tabla 54 - Renderizado en el Back Buffer
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 111 de 134
En la tabla anterior podemos observar el proceso de renderizado de la geometría principal en
el Back Buffer.
Este proceso pasa por etapas como inicialización de parámetros del shader específicos para el
material de la geometría que se está pintando en ese momento. Luego entramos en el bucle
para pintar todas las submallas que intervienen en una malla normal.
En todo este proceso podemos observar cómo se introducen datos tanto en forma de
parámetros lógicos como en forma de texturas. Una vez todos estos parámetros han sido
iniciados para un objeto en concreto, llamamos a la función de renderizado diciéndole cuantos
triángulos tiene el objeto, longitud en memoria de estos, etc.
Pintado de líneas en el motor 3D
m_pD3DDevice->SetTransform( D3DTS_WORLD, &mWorld );
m_pD3DDevice->SetTransform( D3DTS_VIEW, &mView );
m_pD3DDevice->SetTransform( D3DTS_PROJECTION, &mProj );
DebDrawBoundingBoxes();
//Dibujamos los ejes centrados en el (0,0,0)
DebDrawAxis(D3DXVECTOR3(0.0f,0.0f,0.0f),10.0f);
//Dibujamos ejes dobles en el target de la camara
DebDrawAxis(m_Camera.GetTarget(),1.0f,true);
DebDrawLights(0.5f);
DebDrawNormals(5.0f);
DebDrawTangents(5.0f);
DebDrawTriggers();
DebDrawPath();
m_pD3DDevice->SetTransform( D3DTS_WORLD, &mIdentity );
m_pD3DDevice->SetTransform( D3DTS_VIEW, &mIdentity );
m_pD3DDevice->SetTransform( D3DTS_PROJECTION, &mIdentity
Tabla 55 - Renderizado de líneas
En la tabla anterior podemos observa la parte del proceso de renderizado que hace referencia
al pintado de líneas. El pintado de líneas nos es útil para representar en pantalla objetos de
carácter informativo. Un ejemplo de estos objetos pueden ser las Bounding Boxes o las
normales de una malla cualquiera.
A continuación veremos la última parte del proceso de renderizado. Esta parte hacer
referencia al proceso de integración entre todos los renders que hemos hecho anteriormente,
ya sean de geometría principal como de información de carácter de Glow u otros tipos de
información.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 112 de 134
Integración de todos los datos
m_RTmain.LoadFromBackBuffer(0,0,m_iScreenWidth,m_iScreenHeight);
//Configuramos las UV's del Quad que servira de bolcado en
//pantalla de los RT's
SetQuadUVtoRT((m_iScreenWidth >> m_iGlowDownSamples),
(m_iScreenHeight >> m_iGlowDownSamples));
//Extraemos el brillo de la escena con una limite inferior
RenderBright();
//Fusionamos la textura de Self-Ilumination con la de extraccion
//de brillo
RenderSelfPlusBright();
//Aplicamos glow al rendertarget de SelfIlum + el brillo
//extraido de la escena final
RenderGlow();
//Configuramos las UV's del Quad que servira de bolcado en
//pantalla de los RT's
SetQuadUVtoRT(m_iScreenWidth, m_iScreenHeight);
//Fusionamos el render final con la textura resultante de glow
RenderMainPlusGlow();
m_RT1.DumpSurface(0,0,m_iScreenWidth,m_iScreenHeight);
Tabla 56 - Finalización del proceso de render
En esta última tabla podemos observar como guardamos en un Render Target la información
que teníamos en el Back Buffer para poder trabajar con ella posteriormente.
Seguidamente renderizamos el brillo de la escena para que forme parte del Glow, seguido del
render del propio Glow.
Finalizamos con la integración de toda la información y volcamos los gráficos otra vez al Back
Buffer para ser mostrados por pantalla.
3.3 Tratamiento de eventos
3.3.1 Trama
Cuando hablábamos en la sección teórica sobre las tramas, las describimos como
contenedores de información en el que nosotros indicábamos en cada momento que tipo de
dato insertábamos.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 113 de 134
Definición de una trama
class CFrame
{
private:
unsigned int m_iLength;
unsigned int m_iIndex;
char m_sTrama[IL_FRAME_LENGTH];
public:
CFrame ();
~CFrame ();
void AddChar (char inChar);
void AddUChar (unsigned char inChar);
void AddShort (short inShort);
void AddUShort (unsigned short inShort);
void AddInt (int inInt);
//...
};
Tabla 57 - Clase Trama
En la tabla anterior, podemos observar como un objeto trama es definido según su clase.
Podemos observar también cómo existen todas las funciones necesarias para poder introducir
y al mismo tiempo recuperar los datos que necesitemos y del tipo que requiramos.
Por otro lado también podemos observar como todos los datos almacenados son en forma de
cadena de caracteres, fácil de copiar y de transportar.
3.3.2 Eventos
Un evento es un tipo de objeto que en su interior contiene un objeto contenedor de datos en
forma de trama.
Estos objetos de tipo evento son utilizados en el motor para poder transportar eventos de un
lado a otro donde estos sean necesarios, como la entrada de teclado, de ratón, etc.
Cada uno de estos eventos posee, como ya hemos dicho, una trama en su interior, pero al
mismo tiempo posee un identificador único de evento, el cual le permite ser identificado por el
motor 3D y tratado como tal. Recordemos que si la trama interna de un evento no es tratada
como es debido, nos da como resultado información corrupta.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 114 de 134
Definición de un evento
class CEvent
{
private:
unsigned int m_iEventType;
CFrame m_Frame;
public:
CEvent ();
~CEvent ();
void SetType (unsigned int inType);
void SetFrame (CFrame& inFrame);
unsigned int GetType () const;
CFrame& GetFrame ();
const CFrame& GetCFrame () const;
CEvent& operator = (const CEvent& inEvent);
bool operator == (const CEvent& inEvent);
bool operator < (const CEvent& inEvent);
};
Tabla 58 - Clase evento
En la tabla anterior podemos observar el identificador del que hablábamos. Junto a estos
datos, podemos observar todas las funciones de acceso que un evento nos proporciona, desde
insertar una trama dentro del evento, hasta operaciones de igualdad para facilitar su uso de
cara al desarrollador.
3.3.3 Entrada / Salida
Cuando hablamos de entrada y salida de datos a la aplicación, nos referimos a la entrada que
un periférico puede hacer en nuestro motor 3D.
Es por este motivo por el cual mostraremos como tanto el teclado como el ratón generan su
propia información y esta es transformada en eventos hábiles para el motor 3D.
Fragmento de la entrada / salida
case WM_KEYDOWN:
mFrame.AddUInt((unsigned int) wParam);
mFrame.AddUInt((unsigned int) lParam);
mEvent.SetType(IL_KEYDOWN);
mEvent.SetFrame(mFrame);
g_pEngine->SetEvent(mEvent);
gKeybEntrys ++;
break;
Tabla 59 - Transformación de E/S en evento
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 115 de 134
En la tabla anterior podemos observar como el mensaje que nos proporciona el sistema
operativo es transformado en un evento que nuestro motor 3D es capaz de interpretar y
tratar.
3.4 Escena
3.4.1 Vértices flexibles
Cuando hablamos de los vértices flexibles en el apartado teórico, veíamos como estos pueden
cambiar en forma según nuestras necesidades.
Seguidamente observaremos como son definidos y como informamos al motor 3D de su
estructura para que éste pueda interpretarlos.
Definición de un vértice flexible
//--------------------------------------------------------------------
// FVF Format - Static Mesh
//--------------------------------------------------------------------
struct FVFVertex
{
float x, y, z; // The untransformed, 3D position for the vertex
float nx, ny, nz; // The untransformed, 3D normal for the vertex
float tu1,tv1;
float tu2,tv2;
D3DXVECTOR4 Tangent;
};
#define FVF_Format
(D3DFVF_XYZ|D3DFVF_NORMAL|D3DFVF_TEX3|D3DFVF_TEXCOORDSIZE2(0)|D3DFVF_T
EXCOORDSIZE2(1)|D3DFVF_TEXCOORDSIZE4(2))
Tabla 60 - Vértices flexibles
En la tabla anterior podemos observar como nuestro vértice flexible es definido con una serie
de atributos. De igual forma, una vez definido este vértice flexible, nos definimos una etiqueta
que usaremos a lo largo de las instrucciones del motor 3D para informarle de qué contiene
este vértice flexible a estas instrucciones.
Una vez hemos definido el vértice flexible en nuestro motor, solo nos queda informar a
nuestro motor 3D de qué tamaños tienen nuestros atributos en memoria. Esto es sumamente
importante ya que gracias a esto, el motor 3D sabrá moverse por la memoria sin generar
accesos indebidos de ésta.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 116 de 134
Memoria usada por el vértice flexible
const D3DVERTEXELEMENT9 g_VBDecl_Geometry[] =
{ {0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0},
{0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0},
{0, 24, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},
{0, 32, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},
{0, 40, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},
D3DDECL_END()
};
Tabla 61 - Definición de la memoria de un vértice flexible
En la tabla anterior podemos observar cómo definimos la memoria asociada a cada uno de los
vértices flexibles.
Como ya comentábamos, esta definición es esencial para que los accesos a memoria sean
correctos en todo momento.
3.4.2 Vectores de índices y vértices
Una vez tenemos definidos todos los tipos de vértices que vamos a utilizar, es momento de
definir las estructuras que los agruparán para formar tanto los vectores de vértices como los
vectores de índices.
Definición de un vector de vértices
class CVertexArray
{
private:
FVFVertex *m_pVertexArray;
unsigned int m_iNumVertex;
unsigned int m_iId;
public:
CVertexArray ();
CVertexArray (const CVertexArray& inObj);
~CVertexArray ();
void SetId (unsigned int inId);
void SetVertexArray (FVFVertex *inPtr, unsigned
int inNum);
unsigned int GetId () const;
FVFVertex* GetVertexPtr () const;
unsigned int GetNumVertex () const;
void Release ();
CVertexArray& operator= (const CVertexArray& inObj);
bool operator== (const CVertexArray& inObj);
bool operator< (const CVertexArray& inObj);
};
Tabla 62 - Clase VertexArray
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 117 de 134
En la tabla anterior podemos observar la definición de un vector de vértices. Es importante
destacar que este vector es del tipo de vértices que hemos definido previamente y que
contiene todas las operaciones necesarias para poder acceder a la información de dicho
vector.
Definición de un vector de índices
class CIndexArray
{
private:
unsigned short *m_pIndexArray;
unsigned short m_iNumIndex;
unsigned int m_iId;
public:
CIndexArray ();
CIndexArray (const CIndexArray& inObj);
~CIndexArray ();
void SetId (unsigned int inId);
void SetIndexArray (unsigned short *inPtr,
unsigned int inNum);
unsigned int GetId () const;
unsigned short* GetIndexPtr () const;
unsigned short GetNumIndex () const;
void Release ();
CIndexArray& operator= (const CIndexArray& inObj);
bool operator== (const CIndexArray& inObj);
bool operator< (const CIndexArray& inObj);
};
Tabla 63 - Clase IndexArray
Por el contrario en la tabla anterior podemos observar la definición del vector de índices, este
vector de índices irá acompañado indiscutiblemente de un vector de vértices para poder tratar
correctamente su información.
3.4.3 Mallas
Como comentábamos en el apartado teórico, las mallas son un conjunto de submallas que se
unen para formar un modelo al completo.
Cada una de estas submallas tiene su propio material asociado, con lo que nos puede dar
como resultado una malla con diferentes materiales debido a su estructura de submallas.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 118 de 134
Definición de una malla
class CSMesh
{
private:
unsigned int m_iId;
bool m_bVisible;
vector <unsigned int> m_vSubMeshes;
unsigned int m_iPhysicVBId;
unsigned int m_iPhysicIBId;
CBoundingBox m_BoundingBox;
public:
CSMesh ();
CSMesh (const CSMesh& inObj);
~CSMesh ();
void SetId (unsigned int inId);
void SetVisible (bool inVis);
void SetSubMeshId (unsigned int inId);
void SetPhysicVBId (unsigned int inId);
void SetPhysicIBId (unsigned int inId);
void SetBoundingBox (CBoundingBox& inBox);
unsigned int GetId () const;
bool GetVisible () const;
unsigned int GetNumSMeshes () const;
unsigned int GetSubMeshId (unsigned int inIndex)
const;
unsigned int GetPhysicVBId () const;
unsigned int GetPhysicIBId () const;
CBoundingBox& GetBoundingBox ();
const CBoundingBox& GetCBoundingBox () const;
CSMesh& operator= (const CSMesh& inObj);
bool operator== (const CSMesh& inObj);
bool operator< (const CSMesh& inObj);
};
Tabla 64 - Clase CSMesh
En la tabla anterior podemos observar cómo una malla es definida, fijémonos en que contiene
una lista de identificadores a submallas. Esto es así porque es la manera de trabajar en los
managers, pues todo esta indexado para su rápido acceso.
Definición de un manager de mallas
class CSMeshMgr
{
private:
vector<CSMesh> m_vSMeshMgr;
CMaterialMgr *m_pMatMgr;
CSSubMeshMgr *m_pSSubMMgr;
public:
CSMeshMgr ();
~CSMeshMgr ();
unsigned int SetSMesh (CSMesh &inObj);
void SetMatMgrPtr (CMaterialMgr *inPtr);
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 119 de 134
void SetSSubMMgrPtr (CSSubMeshMgr *inPtr);
CSMesh& GetSMesh (unsigned int inIndex);
unsigned int GetNumSMesh () const;
void Release ();
};
Tabla 65 - Clase CSMeshMgr
Aquí podemos ver el manager de mallas, que continúa con la misma metodología y encapsula
objetos malla al mismo tiempo que estos serán accedidos según su identificador.
3.4.4 Submallas
Las submallas son un conjunto atómico de un vector de vértices y un vector de índices. Los dos
vectores dan la suficiente información para que podamos interpretar los triángulos de un
objeto en concreto.
Normalmente al tratarse de una submalla, esta será considerada como una parte de ese
objeto, aunque podría tratarse de un objeto por sí misma.
Definición de una submalla
class CSSubMesh
{
private:
unsigned int m_iVertexBuffer;
unsigned int m_iIndexBuffer;
unsigned int m_iMaterialId;
unsigned int m_iNumVertex;
unsigned int m_iNumFaces;
unsigned int m_iId;
CBoundingBox m_BoundingBox;
public:
CSSubMesh ();
CSSubMesh (const CSSubMesh& inObj);
~CSSubMesh ();
void SetId (unsigned int inId);
void SetVBId (unsigned int inId);
void SetIBId (unsigned int inId);
void SetMatId (unsigned int inMId);
void SetNumVertex (unsigned int inNum);
void SetNumFaces (unsigned int inNum);
void SetBoundingBox (CBoundingBox& inBox);
//...
};
Tabla 66 - Clase CSSubMesh
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 120 de 134
En la anterior tabla podemos observar como una submalla tiene inequívocamente un
identificador de vector de vértices y un identificador de vector de índices.
Por otro lado también podemos observar como la submalla tiene un identificador a un
material al igual como contiene un objeto de Bounding Box para su uso en representaciones
visuales.
Definición del manager de submallas
class CSSubMeshMgr
{
private:
vector<CSSubMesh> m_vSubMeshMgr;
public:
CSSubMeshMgr ();
~CSSubMeshMgr ();
unsigned int SetSSubMesh (CSSubMesh &inObj);
CSSubMesh& GetSSubMesh (unsigned int inIndex);
unsigned int GetNumSSubMesh () const;
void Release ();
};
Tabla 67 - Clase CSSubMeshMgr
En la tabla anterior podemos observar como la definición del manager de submallas se ha
llevado a cabo. Podemos ver como el manager de submallas contiene en su interior un vector
de submallas que será accedido mediante índices para insertar o devolver los objetos
requeridos.
3.4.5 Bounding Boxes
Las Bounding Boxes nos sirven a lo largo de la aplicación para simplificar la forma de los
objetos que nosotros trataremos en nuestro motor 3D. Para eso, es necesario crear una caja
3D con las dimensiones mínimas en las que pueda caber el objeto 3D en concreto.
Para llevar a cabo esta metodología es necesario hacer un pre-cálculo en el que están
involucrados todos los vértices del objeto. Para todos los vértices del objeto, tenemos que
quedarnos con los valores mínimos de esos vértices al igual como los valores máximos.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 121 de 134
El resultado de ese punto mínimo y máximo nos definirá una caja 3D con volumen mínimo
para que pueda caber dicho objeto.
Definición de una Bounding Box
class CBoundingBox
{
private:
unsigned char m_cUsedVertex;
D3DXVECTOR3 m_vUpperVertex;
D3DXVECTOR3 m_vLowerVertex;
protected:
void CheckVertex ();
public:
CBoundingBox ();
CBoundingBox (const CBoundingBox&);
~CBoundingBox ();
void SetUpperVertex (D3DXVECTOR3& inV);
void SetLowerVertex (D3DXVECTOR3& inV);
void SetCenterAndSize (D3DXVECTOR3& inV, float
inHalfSize);
bool IsInside (D3DXVECTOR3& inV);
bool IsInside (float in1, float in2, float
in3);
bool IsVectorCollide (D3DXVECTOR3&
inPos,D3DXVECTOR3& inDir);
const D3DXVECTOR3& GetUpperVertex () const;
const D3DXVECTOR3& GetLowerVertex () const;
D3DXVECTOR3 GetCenter ();
D3DXVECTOR3 GetUpperCenter ();
D3DXVECTOR3 GetLowerCenter ();
unsigned char GetUsedVertex () const;
float GetVolume () const;
void Reset ();
CBoundingBox& operator= (const CBoundingBox& inObj);
bool operator== (const CBoundingBox& inObj);
bool operator< (const CBoundingBox& inObj);
};
Tabla 68 - Clase CBoundingBox
En la tabla anterior podemos observar la definición de un objeto de tipo Bounding Box. En esta
definición podemos observar cómo la Bounding Box está formada por dos puntos 3D que son
los que crearan la caja 3D.
Por otro lado tenemos funciones de acceso a los datos los cuales facilitan la vida al
desarrollador.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 122 de 134
3.4.6 Cámaras
En nuestro motor 3D, la cámara es el objeto encargado en todo momento de gestionar las tres
matrices más importantes de la escena. Nos referimos a la matriz de mundo, visión y
proyección.
La cámara encapsula el control de estas tres matrices y aparte da una funcionalidad gracias a
su interfaz para que el desarrollador pueda acceder rápidamente a controles de dirección de la
cámara, apertura de esta, etc.
Definición de la cámara
class CCamera
{
private:
unsigned int m_iId;
unsigned int m_iType;
D3DXMATRIX m_mView;
D3DXMATRIX m_mProjection;
D3DXMATRIX m_mViewProjection;
//...
float m_fPitch;
public:
CCamera ();
~CCamera ();
void SetId (unsigned int inId);
void SetCameraType (unsigned int inType);
void SetViewMatrix (D3DXMATRIX& inVMatrix);
void SetViewMatrixInv (D3DXMATRIX& inVMatrixI);
void SetProjMatrix (D3DXMATRIX& inPMatrix);
void SetViewProjMatrix (D3DXMATRIX& inVPMatrix);
void RotateCamera (float inDx, float inDy);
void TranslateCamera (D3DXVECTOR3& inVec);
//...
unsigned int GetId () const;
unsigned int GetCameraType () const;
const D3DXMATRIX& GetViewMatrix () const;
const D3DXMATRIX& GetViewMatrixInv () const;
const D3DXMATRIX& GetProjMatrix () const;
const D3DXMATRIX& GetViewProjMatrix () const;
CCamera& operator= (const CCamera& inObj);
bool operator== (const CCamera& inObj);
bool operator< (const CCamera& inObj);
};
Tabla 69 - Clase CCamera
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 123 de 134
Como podemos observar en la tabla anterior, una cámara contiene muchos datos en su
interior y gracias a esto, es capaz de darnos en todo momento información sobre las tres
matrices esenciales que ésta controla.
De igual forma, gracias a la interfaz de las cámaras, podemos usarlas de una forma sencilla y
eficiente.
También es necesario comentar que en la tabla anterior, la interfaz ha sido simplificada debido
a su tamaño en funciones y datos. En el código fuente se pueden encontrar en su forma
completa.
3.4.7 Frustrums
Cuando hablamos de frustrums en el apartado teórico, pudimos ver que estos nos ayudan a
determinar qué objetos están dentro del área de visión y qué objetos no.
Para generar un frustrum es necesario crear seis planos que nos delimiten un volumen. Por
ello a continuación veremos la interfaz del objeto frustrum.
Definición del objeto frustrum
class CFrustrum
{
private:
D3DXPLANE m_Left;
D3DXPLANE m_Right;
D3DXPLANE m_Bottom;
D3DXPLANE m_Top;
D3DXPLANE m_Near;
D3DXPLANE m_Far;
public:
CFrustrum ();
~CFrustrum ();
void SetFrustrum (D3DXMATRIX& inMat);
bool IsPointInFrustrum (D3DXVECTOR3& inVec);
bool IsPointInFrustrum (float inX, float inY, float inZ);
unsigned int IsBBoxInFrustrum (CBoundingBox& inBBox, bool
inExact = false);
};
Tabla 70 - Clase CFrustrum
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 124 de 134
En la tabla anterior podemos observar cómo el frustrum está constituido básicamente por seis
planos.
Cada uno de estos seis planos es generado gracias a una matriz de entrada. Concretamente
esta matriz viene determinada por la cámara.
Una vez tenemos los seis planos determinados, es trivial determinar si un punto 3D está
contenido dentro del volumen de frustrum o no.
Podemos observar también que en la interfaz existen funciones que nos determinan si un
punto esta contenido dentro del volumen o no para ayudar al desarrollador.
3.5 Sombras
3.5.1 Cube Shadow Maps
Tal y como comentábamos en el apartado teórico, las sombras de este motor 3D se han hecho
a partir de un tipo de sombras llamadas Shadow Maps. Y hemos extendido esta teoría para
usarlas como Cube Shadow Maps.
Un Cube Map no difiere mucho del anterior objeto del que hemos hablado. Los dos tienen seis
partes primordiales en su interior, donde el frustrum tenía planos, en el Cube Map tenemos
superficies de renderizado.
Datos internos del Cube Map
class CFrustrum
{
LPDIRECT3DDEVICE9 m_Device;
LPD3DXRENDERTOSURFACE m_RenderToSurface;
LPDIRECT3DSURFACE9 m_Surface;
D3DFORMAT m_ColorFormat;
D3DFORMAT m_StencilZBufferFormat;
LPDIRECT3DCUBETEXTURE9 m_CubicShadowMap;
LPDIRECT3DSURFACE9 m_DepthCubeFacePX;
LPDIRECT3DSURFACE9 m_DepthCubeFacePY;
LPDIRECT3DSURFACE9 m_DepthCubeFacePZ;
LPDIRECT3DSURFACE9 m_DepthCubeFaceNX;
LPDIRECT3DSURFACE9 m_DepthCubeFaceNY;
LPDIRECT3DSURFACE9 m_DepthCubeFaceNZ;
D3DXMATRIX m_mViewProjPX;
D3DXMATRIX m_mViewProjPY;
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 125 de 134
D3DXMATRIX m_mViewProjPZ;
D3DXMATRIX m_mViewProjNX;
D3DXMATRIX m_mViewProjNY;
D3DXMATRIX m_mViewProjNZ;
D3DXVECTOR3 m_vPosition;
CCamera m_CamHelper;
unsigned int m_iSurfaceSize;
bool m_bUseDepthBuffer;
bool m_bReleased;
//...
Tabla 71 - Datos de la Clase CCubeMap
Como podemos observar en la tabla anterior, los datos relacionados con el Cube Map son en
su mayoría las seis superficies de renderizado asociadas a cada una de las caras del Cube Map.
Junto a esto también podemos comentar que internamente el Cube Map mantiene las seis
matrices de visión con proyección en cada una de las seis direcciones que puede tener un Cube
Map.
A continuación mostramos la interfaz del Cube Map para hacernos una idea de las
instrucciones que se pueden llevar a cabo con él.
Funciones del Cube Map
//...
CCubeMap ();
~CCubeMap ();
bool CreateTexture ( LPDIRECT3DDEVICE9
inDev, unsigned int inSize, D3DFORMAT inColorFormat = D3DFMT_A8R8G8B8,
D3DFORMAT inStencilFormat = D3DFMT_UNKNOWN);
void BeginScenePX ();
void EndScenePX ();
//...
void BeginSceneNZ ();
void EndSceneNZ ();
void SetCubeMapPosition (D3DXVECTOR3& inPos);
void LoadFromFile (string inPath);
void SaveCubeMapToFile (string inName);
void SavePXToFile (string inName);
//...
void SaveNZToFile (string inName);
void Release ();
D3DXMATRIX GetViewProjMatrixPX ();
//...
D3DXMATRIX GetViewProjMatrixNZ ();
LPDIRECT3DCUBETEXTURE9 GetCubeTexture () const;
};
Tabla 72 - Interfaz de la Clase CCubeMap
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 126 de 134
En la tabla anterior podemos observar como casi todas las funciones están repetidas seis
veces, haciendo referencia a las seis superficies de renderizado o a las seis matrices específicas
de cada cara.
De igual forma también podemos observar como la función de creación del Cube Map solo
acepta una medida para este Cube Map. Esto es así debido a que un Cube Map tiene forma de
cubo, y por esto no puede ser más largo de un lado que de otro, es decir, todos los lados
medirán lo mismo.
3.6 Exportador
En el apartado teórico comentábamos que el exportador es una aplicación clave para el motor
3D. El exportador toma tanta importancia debido a que sin él sería imposible generar la
geometría específica para representarla en pantalla.
Él es el encargado de generar la geometría con el formato especifico necesario que nuestro
motor 3D entiende. Para llegar a este punto, el exportador sigue una serie de procesos de
lectura, transformación y escritura de una entrada para llegar a una salida en concreto.
A continuación mostraremos los grandes bloques de los que el exportador hace uso para su
trabajo.
Inicialización de los datos
for(unsigned int m = 0; m < (iMaxMatId + 1); m++)
{
vFaces.clear();
vFacesReOrder.clear();
vNumFaces.clear();
vPositions.clear();
vNormals.clear();
vTextureCoord1.clear();
vTextureCoord2.clear();
vUsedVertex.clear();
//...
Tabla 73 - Inicialización del constructor
En la tabla anterior podemos ver como se inicializan los datos del constructor. Esto es
necesario ya que cada objeto que analice el constructor tendrá que empezar con una memoria
limpia.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 127 de 134
Recreación de los Vertex y Index Buffers
//...
if(m == (unsigned int)m_AseObject.m_vMeshObj[i].m_vMeshMaterialId[k])
{
vNumFaces.push_back(m_AseObject.m_vMeshObj[i].m_vIndexBuffer[k].GetX());
vNumFaces.push_back(m_AseObject.m_vMeshObj[i].m_vIndexBuffer[k].GetY());
vNumFaces.push_back(m_AseObject.m_vMeshObj[i].m_vIndexBuffer[k].GetZ());
vFaces.push_back(m_AseObject.m_vMeshObj[i].m_vIndexBuffer[k]);
}
//...
Tabla 74 - Vertex y Índex Buffers en el constructor
Aquí podemos observar como leyendo el fichero de entrada se generan los vectores de
vértices y los vectores de índices de la geometría que estamos tratando.
Formato ASE a formato iLengine
//...
for(unsigned int p=0;p<(unsigned int)vNumFaces.size()&& !bWorked; p++)
{
if(vFaces[l].GetZ() == vNumFaces[p])
{
iIndex = p;
if(vUsedVertex[iIndex])
{
//...
Tabla 75 - Cambio de formato de vectores
En la tabla anterior podemos observar como cambiamos el formato de cada uno de los vértices
de entrada a los vértices que nosotros necesitemos. Es necesario comentar que el formato ASE
tiene una forma peculiar de almacenar la información, y necesitamos transformar esa forma a
nuestro formato.
Rotación de las normales
//...
vSwapRotVec = m_AseObject.m_vMeshObj[i].GetRotation();
fRotAngle = m_AseObject.m_vMeshObj[i].GetRotationAng();
fRotAngle = -fRotAngle;
//...
Tabla 76 - Rotación de las normales
Por alguna razón que desconocemos, las normales del formato ASE no vienen rotadas, y para
eso es necesario rotarlas antes de operar con ellas.
Una vez tenemos todas las geometrías exportadas a nuestro formato específico, es momento
para juntar las coordenadas de textura de aquellos objetos que sean iguales.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 128 de 134
Las coordenadas de textura se juntan debido a que necesitamos más de un mapa de texturas
por objeto en pantalla. Para poder aplicar light maps y diversas técnicas más.
Escribiendo en disco
//...
oString << "\n\t\t\t\t\t\t<vertex num=\"" << k << "\">";
//Si lo queremos normal el objeto lo cargaremos mediante este codigo
oString << "\n\t\t\t\t\t\t\t<position x=\"" << fixed <<
vGroupMeshes[i][j][k].GetPosition().GetX() <<"\" y=\"" << fixed <<
vGroupMeshes[i][j][k].GetPosition().GetY() <<"\" z=\"" << fixed <<
vGroupMeshes[i][j][k].GetPosition().GetZ() <<"\" />";
oString << "\n\t\t\t\t\t\t\t<normal x=\"" << fixed <<
vGroupMeshes[i][j][k].GetNormals().GetX() <<"\" y=\"" << fixed <<
vGroupMeshes[i][j][k].GetNormals().GetY() <<"\" z=\"" << fixed <<
vGroupMeshes[i][j][k].GetNormals().GetZ() <<"\" />";
oString << "\n\t\t\t\t\t\t\t<texcoord u=\"" << fixed <<
vGroupMeshes[i][j][k].GetTexture1().GetX() <<"\" v=\"" << fixed <<
vGroupMeshes[i][j][k].GetTexture1().GetY() <<"\" />";
oString << "\n\t\t\t\t\t\t\t<texcoord2 u=\"" << fixed <<
vGroupMeshes[i][j][k].GetTexture2().GetX() <<"\" v=\"" << fixed <<
vGroupMeshes[i][j][k].GetTexture2().GetY() <<"\" />";
oString << "\n\t\t\t\t\t\t\t<tangent x=\"" << fixed <<
vGroupMeshes[i][j][k].GetTangent().GetX() <<"\" y=\"" << fixed <<
vGroupMeshes[i][j][k].GetTangent().GetY() <<"\" z=\"" << fixed <<
vGroupMeshes[i][j][k].GetTangent().GetZ() <<"\" w=\"" << fixed <<
vGroupMeshes[i][j][k].GetTangentW() <<"\" />";
oString << "\n\t\t\t\t\t\t</vertex>";
//...
Tabla 77 - Guardando la nueva geometría
En la tabla anterior podemos observar como guardamos toda la información procesada en un
fichero que será el que leeremos en un futuro como formato propio.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 129 de 134
4 Resultados
Monitores con Glow Tuberías con Specular + Normal
Cubos con diferentes materiales Multi-material en un cubo
Material complejo Materiales en movimiento
Sala de reactores Reactor
Tabla 78 - Primera tabla de resultados
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 130 de 134
Enredaderas Enredaderas + Cielo
Personaje UT complejo Personaje UT simple
Túnel + Niebla efecto calor Túnel + Niebla efecto frio
Catedral Vidrieras
Tabla 79 - Segunda tabla de resultados
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 131 de 134
5 Conclusión
Una vez finalizado el proyecto, nos sentimos orgullosos de los resultados obtenidos al igual
como haber aprendido el uso y funcionalidades de la librería gráfica usada.
Hemos realizado con éxito un sistema de iluminación dinámica por píxel que nos ofrece una
calidad visual muy elevada juntamente con la iluminación global que nos ofrece la
componente de light map en los objetos de la escena.
El efecto experimental de la interpolación de colores en el sistema de la niebla ha dado unos
muy buenos resultados. Este sistema ahora es capaz de ofrecer las ventajas de una niebla
normal y corriente al mismo tiempo que ayuda a la integración global de la escena y ayuda a
transmitir mejor una sensación en ésta.
Las sombras que hemos implementado cumplen perfectamente su función, y aunque en
motores 3D profesionales existen soluciones de mayor calidad visual, estamos conformes por
ser una primera aproximación a las soluciones de sombras.
El sistema de post procesado funciona correctamente y el efecto resultante es de mayor
complejidad que los estudiados previamente, ofreciendo una calidad cercana a muchos
motores 3D profesionales si no igual en ciertos aspectos.
Las optimizaciones de visión de geometría están a la altura de todo el proyecto, ofreciendo en
todo momento un rendimiento óptimo para la demo en tiempo real. Las oclusiones visuales de
geometría funcionan correctamente y el primer acercamiento a éstas ha sido todo un acierto.
El sistema de eventos funciona debidamente y nos proporciona una alta funcionalidad a la
hora de gestionar acciones generadas tanto por parte del usuario como por el propio entorno.
Los materiales son otro gran logro de este proyecto, pues nos han dotado de una gran sencillez
a la hora de crear objetos de una gran complejidad en escena.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 132 de 134
6 Líneas de futuro
Un proyecto tan complejo como este puede tener unas muy variadas líneas de futuro, aunque
expondremos seguidamente las más importantes.
Una de las cosas que más puede recibir cambios es la ecuación de iluminación, y así es como
debería ser. Actualmente la ecuación de iluminación acepta solo una luz dinámica, y estaría
bien que pudiese aceptar más de una.
Esta misma ecuación de iluminación juntamente con los materiales podrían ofrecer en un
futuro la capacidad mediante máscaras de juntar diferentes materiales en una misma submalla
para poder simplificar la forma de hacer terrenos u objetos que necesiten de esa característica.
Se podría añadir a la ecuación de iluminación la componente de reflexión mediante un Cube
Map diseñado a tal efecto, revisando en todo momento la carga gráfica que esto supondría
para que no perjudique a todo el sistema.
Una línea de futuro para las sombras sería el hecho de usar una textura de ruido diseñada
específicamente para poder suavizar los bordes de las sombras y así poder ofrecer mejor
calidad visual de éstas en tiempo real.
Una de las carencias que este motor 3D tiene a raíz de la complejidad del sistema de
sombreado que posee es la falta de materiales con transparencia. Esta sería una línea de
futuro interesante en la que actuar, posiblemente haciendo que las sombras pudieran ofrecer
la característica de respetar las transparencias en su proceso de creación.
Otra línea de futuro seria implementar un sistema de partículas acorde con las ecuaciones de
iluminación que posee este motor 3D. Con los sistemas de partículas se pueden hacer hoy en
día muchos efectos y ayudar a generar muchas ambientaciones tal y como lo hacen ahora
efectos ya creados, pero con menor aportación en conjunto que sin los sistemas de partículas.
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 133 de 134
7 Glosario
Siglas Descripción
XP Terminación del nombre Windows XP, referente a experiencia en inglés.
SDK Kit de desarrollo de software, las siglas provienen del inglés.
3D Referente a tercera dimensión o tres dimensiones.
CS Terminación del nombre comercial del programa Photoshop
API Application Programming Interface, Interfaz de programación de la aplicación.
VESA Video Electronics Standards Association, Asociación de estándares de video electrónico.
C++ Lenguaje de programación evolucionado de C
DLL Dynamic Link Library, Librería de links dinámicos
DXUT DirectX Utility Toolkit, Caja de utilidades de DirectX
Proyecto iL-Engine Enginyeria i Arquitectura La Salle
Página 134 de 134
8 Bibliografía
1. Microsoft Corporation. DirectX Developer Center. [En línea] 2008. [Citado el: 10 de Junio de 2008.] http://msdn.microsoft.com/en-us/directx/default.aspx. 2. —. DXUT Programming Guide. [En línea] 2008. [Citado el: 11 de Junio de 2008.] http://msdn.microsoft.com/en-us/library/bb173316(VS.85).aspx. 3. Epic Games. Unreal Technology. [En línea] 2008. [Citado el: 10 de Junio de 2008.] http://www.unrealtechnology.com/. 4. Crytek. Crytek website. [En línea] 2008. [Citado el: 10 de Junio de 2008.] http://www.crytek.com/. 5. Torus Knot Software. OGRE 3D : Open source graphics engine. [En línea] 2008. [Citado el: 10 de Junio de 2008.] http://www.ogre3d.org/. 6. Crystal Space 3D. Crystal Space website. [En línea] 2008. [Citado el: 10 de Junio de 2008.] http://www.crystalspace3d.org/main/Main_Page. 7. Wikipedia. C++. [En línea] 2008. [Citado el: 18 de Julio de 2008.] http://es.wikipedia.org/wiki/C%2B%2B. 8. Autodesk, Inc. Autodesk 3ds Max. [En línea] 2008. [Citado el: 10 de Junio de 2008.] http://www.autodesk.es/adsk/servlet/index?siteID=455755&id=10611609. 9. VESA. VESA website. [En línea] 2008. [Citado el: 10 de Junio de 2008.] http://www.vesa.org/. 10. Creative. Creative Website. [En línea] 2008. [Citado el: 10 de Junio de 2008.] http://es.europe.creative.com/. 11. Wikipedia. Phong Shading. [En línea] 2008. [Citado el: 12 de Junio de 2008.] http://en.wikipedia.org/wiki/Phong_shading. 12. —. Oren-Nayar Reflectance Model. [En línea] 2008. [Citado el: 12 de Junio de 2008.] http://en.wikipedia.org/wiki/Oren%E2%80%93Nayar_Reflectance_Model. 13. AMD - Ati. Advanced Micro Devices Inc. [En línea] [Citado el: 3 de julio de 2008.] http://ati.amd.com/developer/gdce/oat-scenepostprocessing.pdf.