|
Diseño de un motor 3d moderno En esta serie de artículos os voy a hablar sobre el desarrollo de un motor de render para videojuegos. Para ello nos hemos juntado Iñigo Quilez (conocido por sus trabajos en shader avanzados como el ambient occlusion en espacio de pantalla), Jesús de Santos (estuvo en la parte de tecnología de pyros studios y ahora trabaja como independiente) y el que escribe, Javier Loureiro (actualmente desarrollando el motor de render para ilion animation studios), para que cada uno aporte la visión que consideramos importante a la hora de desarrollar un motor 3D orientado a tarjeta gráfica.
Un motor realtime 3D se encarga básicamente de varias cosas, que hay que pensar detalladamente. Lo primero es mantener una descripción de la escena, con los distintos elementos que la forman, su estado, y las relaciones entre distintas entidades. Es lo que se denomina scenegraph, y puede ser que tengamos incluso distintos scenegraphs para determinadas tareas. Al scenegraph se le piden "nodos" que podremos modificar, y después el motor se encargara de pintar correctamente.
Una primera tarea es definir correctamente la descripción de la escena, y para ello normalmente se utilizan exportadores y plugins para programas 3D comerciales, como 3dsmax, XSI, o maya. Estos programas nos van a dar una cantidad de información enorme: geometría, materiales, los huesos de la animación, todo tipo de propiedades que deseemos agregar al objeto, etc. Nosotros tenemos que decidir que vamos a implementar, como vamos a permitir editarlo en el software 3d, y tendremos que pensar la mejor forma de exportar esa información a nuestra definición de escena. Este capítulo no es en sí nada trivial. Muchos de los grandes errores de un diseño del motor vienen de no pensar adecuadamente dónde almacenar los datos. Por eso lo ideal es que al principio diseñemos una forma flexible de modificar este fichero. Una idea para conocer cual es la mejor forma de almacenar los datos es investigar varios programas de 3D (maya,XSI y 3dsmax al menos), y ver qué datos se almacenan en los objetos. Este tipo de investigaciones os van a servir de mucho posteriormente, ya que tendréis claros los requisitos posteriores del motor. El proceso normal de un no iniciado en el desarrollo de un motor de render es comenzar poco a poco a implementar cosas que se ven en pantalla. Es una forma rápida de ver resultados, pero tiene un problema, y es que te encuentras continuamente reformando el motor para implementar esa característica que no habías pensado antes. El investigar las aplicaciones 3D nos va a reducir esta pérdida de tiempo posterior. Hay que tener especial cuidad con los datos de materiales, que es algo fundamental para la tarjeta, y nos van a condicionar bastante en el futuro. Los programas de 3D actuales son bastante complejos en ese aspecto, así que tendréis trabajo para rato.
Nuestro programa (videojuego, demo, lo que sea) se encarga de comunicarse con la escena, normalmente de forma asíncrona (esto es, en cualquier momento, el estado de un nodo puede cambiar). Por ejemplo, en un videojuego de coches, puede pasar que la carrocería se deteriore, y tengamos que cambiar el modelo, o puede que un objeto en nuestra demo necesite cambiar de posición, o simplemente, reproducir una animación. Esto ya nos abre la via de los threads y la sincronización entre ellos. Podemos tener un thread sólo para pintar, y el juego estará "pensando" en otro thread, o gestionando los eventos del usuario, el paso del tiempo, etc. Esto se puede hacer en 1 solo hilo, pero hoy en día es común dividir las tareas en múltiples threads. Por ejemplo, la física suele estar en otro thread, la carga de texturas en otro dedicado, etc.
En el momento que decidimos pintar nuestra definición de escena, pasamos a "visitarla", recorriendo la definición pero no para pintar inmediatamente, si no para generar unas listas de datos en bruto que mandaremos a la tarjeta. Visitar es simplemente ir recorriendo los nodos de nuestra definición de escena, ejecutando unas funciones. Este proceso en 2 etapas es fundamental para entender cómo se genera un motor de render óptimo y cómo funcionan las tarjetas modernas.
El proceso es el siguiente. Imaginad que sólo tenemos nodos con una geometría, y una matriz de transformación. Visitamos el nodo raíz, leemos su matriz como matriz "actual", e insertamos el nodo con esa matriz en la lista. Vamos a su primer hijo. Leemos su matriz, y la multiplicamos por la de su nodo padre. Insertamos ese nodo en la lista, con la matriz multiplicada. Vamos así recorriendo los nodos (usando una pila para cuando tengamos que subir en la jerarquía). Al final tendremos una lista lineal que el API de la tarjeta ira pintando secuencialmente, con un "loadmatrix".
Esto tiene muchísimas ventajas. Una es separar la lógica de los gráficos. La definición de escena puede tener nodos del tipo "coche", "árbol", etc, pero al visitarlos, y crear la lista de objetos "renderizables", todos son mallas de vértices y caras. Al visitarlos, podemos descartar los objetos que no se van a ver, etc. Es importante el concepto moderno de objeto renderizable. Es importante que cada objeto en la lista tenga todas las propiedades necesarias para ser pintado, ya que es mucho mas optimo pintar los objetos agrupados por esas propiedades.
Una vez que tenemos las listas, le mandamos al driver gráfico la lista secuencial. El termino no deberíamos de confundirlo con el driver de la tarjeta. Nuestro propio driver es el que sabe como pintar, y se encargara de cosas como compilar el shader, enviar las texturas, cambiar los estados de render, leer las preferencias del usuario para pintar, etc. Aquí podemos generalizar la función "drawmesh" para que se encargue de seleccionar entre opengl/directx, etc. La idea es que el visitor tenga todo machacado, y que el driver simplemente pinte a lo tonto, sin preguntarse si debe o no hacerlo. De esta forma, todo va a ir mas rápido y sera fácil meter cambios en el motor (y poder reaprovecharlo en el futuro).
Por lo que al pensar en hacer un motor gráfico, debéis de repartir esas 4 tareas: exportado, scenegraph, visitor, driver gráfico. Cada tarea es en si bastante independiente (dentro de lo que cabe, claro), y mantener estas partes bien diferenciadas nos permitirá tener un motor mucho más flexible y fácil de mantener.
Una guía de desarrollo sería la siguiente:
a) Investigación.
Primero trabajar con varios programas 3D, y escribir algún tipo de exportador python para ellos (maya/xsi/luxology modo lo soportan, 3dsmax tiene su propio maxscript). Hacer un volcado de la escena a un fichero de texto es un muy buen ejercicio para tener claro las especificaciones del motor. Tarde o temprano tendréis que acceder a una aplicación 3D, y el leer sobre los datos que exporta el programa os dará una idea para acotar las posibilidades reales del motor (que la ignorancia suele definir en "infinitas")
Otro tema importante es mirar y conocer cómo trabajan otros motores de render. Ogre3D y el Irrlich son buenos candidatos para estudiar. yo comenzaría a mirar el irrlich, y después el ogre. Hacerse los tutoriales e implementar algún efecto nos ayudará a entender las problemáticas que tendremos que afrontar y ver cómo funcionan otras APIs. Cuanto más motores podáis analizar antes de comenzar programar el vuestro, mucho mejor, porque veréis las enormes diferencias entre ellos, y donde se va la carga de desarrollo. Es interesante que comentéis de estas investigaciones con otra gente para que os de ideas, y puntos de vista que no habíais pensado antes. Conocer un motor a fondo requiere tiempo.
b) Desarrollar vuestro propio driver gráfico. De eso hablaremos profundamente en el siguiente articulo. La idea es tener una librería de bajo nivel que vaya pintando listas de objetos previamente definidos. Podremos generar las listas de forma artesanal, o de forma precalculada. Por ejemplo, podremos comenzar a desarrollar una demo puramente gráfica, sin ningún tipo de iteractividad, pero muy eficiente pintado polígonos. Lo ideal es reducir el numero de características que vamos a pintar al principio, pero las pocas que tenga, que realmente funcionen muy bien y que este bien probada. No borres la demo porque te servirá para testear futuros desarrollos, independientemente del juego o aplicación que vayas a utilizar.
c) Desarrollar el vistante de scenegraph. Para esto dedicaremos el ultimo capitulo. Tenemos que ir leyendo algún parámetro adicional que cambie el estado del motor (posición del jugador, iteracion, físicas, etc) y nuestro programa generara la información necesaria para el driver grafico. En esta etapa, debemos de elegir bien que ira al driver y que podremos precalcular en el scenegraph.
d) Comenzaremos a implementar nuestra aplicación, que puede ser un juego, una demo interactiva o lo que sea. El proceso debería de ser iterativo. Ir probando las partes mas sencillas del motor con el artista, corregir los fallos que aparecen, y meter un nivel mas de complejidad. Por ejemplo, podemos comenzar con solo 1 shader y sin luces, e ir ampliando la lista de caracteristicas poco a poco. Primero metemos código de iluminación en el driver gráfico, metiendo directamente la info requerida por nosotros mismo, para después definirla desde el scenegraph, y posteriormente, a petición de la aplicación.
Espero que el articulo os haya entretenido, y os vengan las ganas de probar y comenzar un sencillo motor.
This e-mail address is being protected from spam bots, you need JavaScript enabled to view it
blog Jesús de Santos blog de Iñigo Quilez
|