miércoles, 7 de mayo de 2008

Sincronizando mapa de alturas gráfico + físico

Una de nuestras primeras decisiones ha sido utilizar los mapas o campos de alturas como geometría para describir el terreno del simulador ya que los motores en los que nos apoyamos incorporan este tipo de geometrías en sus versiones más recientes. Así que vamos a realizar una mini-guía para saber como define la geometría cada uno de los motores y como podemos sincronizarlas para que la colisión proporcionada por el motor físico coincida con la representación gráfica que realiza el motor gráfico.



Introducción

Los motores que utilizamos en nuestro proyecto son: Irrlicht como motor gráfico y ODE como físico.
La terminología para referirse a los mapas de alturas es algo diversa, encontrándonos con términos como: terrain, heightfield, heightmap o sus respectivas traducciones al castellano. En general, Irrlicht los define como terrains y ODE como heightfields. Pero todos hacen referencia a exactamente el mismo tipo de geometría.

Por si alguien anda perdido sobre qué es un campo de alturas, la idea más sencilla es que se trata de un área (cuadrada normalmente) cuya altura en cada punto viene definida por una función o algún otro tipo de representación. Por eso su utilidad más directa es la de representar terrenos tridimensionales.

Irrlicht Terrain Scene Node

Para éste motor gráfico, un campo o mapa de alturas es una geometría de tipo TerrainSceneNode.
La representación de la altura es obtenida directamente de una imagen en escala de grises donde el blanco indica el punto más alto y el negro el punto más bajo. Como un color no define una altura concreta, debemos entender la representación de la altura como algo proporcional (entre el blanco y el negro, entre 0 y 1..), de forma que la altura final deseada se consigue mediante la aplicación de un escalado. Esto nos permite representar el mismo campo de alturas de forma suave o abrupta.
Recordad que al estar en entornos 3D (X,Y,Z), por defecto el terreno se representa en las coordenadas (X,Z) de forma horizontal y la altura viene dada por la componente (Y).
Algo importante a tener en cuenta es que Irrlicht define la esquina inferior izquierda del terrain como el origen de coordenadas de la geometría.

ODE Heightfield Geom

La definición del campo de alturas por parte del motor físico es algo más delicada.
El tipo de datos utilizado es un geomheightfield, y espera recibir los datos de la componente altura desde una función matemática. Por lo tanto, hay que codificar un método en nuestra aplicación que haga de callback para el campo de alturas, indicando el valor de la componente (Y) para un par (X,Z) dado.
(Si tienes problemas para utilizar un método (C++) como función de callback (C) encontraras la solución aquí.)
La principal sutileza de utilizar una función matemática como fuente de datos para las alturas es que los valores de entrada de la función son enteros, de este modo ODE construye su heightfield mediante coordenadas del tipo: { (0,0), (0,1), (0,2) ... (m,n) }, siendo independientes a las coordenadas (X,Z) del mundo tridimensional. Y algo que puede ser ahora obvio, es la necesidad de indicarle al motor físico el número de divisiones (en sus 2 dimensiones (X,Z)) de nuestro heightfield. Esas divisiones discretizan el área definida y corresponden a las coordenadas enteras utilizadas en el callback (sus valores máximos: (m,n)).
Otro detalle a tener en cuenta, sobre todo de cara a la sincronización, es que para ODE el origen de coordenadas de la geometría obtenida se sitúa en el centro (X,Z) del campo de alturas generado.

Synching Irrlicht-ODE Heighfields

La forma más fácil de afrontar el problema es generar inicialmente el terreno con Irrlicht mediante una imagen BMP que podamos modificar a nuestro antojo, y a partir de él obtener la componente altura para ODE mediante el método getHeight.

Como hemos visto, Irrlicht y ODE localizan el origen de coordenadas de la geometría en puntos distintos, con lo que habrá que corregir este desfase para que la representación de ambos por pantalla coincida.
En resumen, la posición de la geometría de ODE vendrá dada por la siguiente función:
(X,Z)_ODE = (X,Z)_IRR + (Width,Depth)/2

Irrlicht trabaja siempre con las coordenadas del mundo tridimensional, por lo que nuestro campo de alturas está correctamente localizado por su parte. Al construir el terrain definiremos su altura mediante un mapa de bits y su tamaño mediante el escalado oportuno. Y para construir el heightfield de ODE obtendremos las características directamente del terrain de Irrlicht.
El resto de parámetros del constructor por parte de Irrlicht afectan a los algoritmos de renderizado del terreno y no a su estricta definición, con lo cual no son importantes de cara a la sincronización con ODE.
En la construcción del heightfield de ODE tendremos que indicar el método callback que va a proporcionar la altura del terreno, las dimensiones del mismo que obtendremos de nuestro terrain ya construido, y las divisiones en las que queremos discretizar el área del heightfield. Estos últimos valores son independientes a Irrlicht y tendremos que sacárnoslos de la manga. Básicamente estamos definiendo el detalle y complejidad del terreno para nuestro motor físico, así que cuantas más divisiones hagamos más se ajustará la representación física del terreno a la representación gráfica, pero más cálculos físicos tendremos y más lenta irá la simulación. Clásico tradeoff de optimización, así que todo depende de la calidad de los resultados que requiramos en un momento dado. También acepta ODE un escalado en el constructor, pero como nuestro terreno ya está completamente generado por Irrlicht, debemos privarle a ODE de hacer modificaciones sobre los datos que le vamos a proporcionar, por lo tanto indicaremos siempre que NO haga escalado (lo que supone un valor real de 1.0).

Y por último, sólo quedaría definir el callback para completar la sincronía entre ambos motores.
Éste es un punto delicado ya que recordemos que Irrlicht trabaja con las coordenadas del mundo y ODE con sus componentes enteras en las que ha discretizado el terreno. Por lo tanto, la idea es transformar las coordenadas enteras que utiliza ODE en la llamada del callback en coordenadas del mundo para obtener de Irrlicht la altura correcta en el punto deseado. Teniendo esto claro y recordando los atributos que han entrado en juego no debe suponer un problema ver la solución:

anchuraDivision = (Width,Depth) / divisiones
(X,Z)_IRR = (X,Z)_ODE * anchuraDivisión + (X,Z)_POSITION

Con anchuraDivision obtenemos la distancia en coordenadas del mundo que está discretizando ODE entre entero y entero. Ya solo falta multiplicar por el entero dado y desplazar la coordenada a donde hayamos colocado nuestro terrain en el entorno tridimensional.

Demo

En el Box podéis encontrar la demo test_terrain-balls.zip en la que se muestra la sincronización explicada. Hay representado un terreno y una matriz de pelotas cae del cielo colisionando contra él. Es posible mover la cámara y desplazarse por el mundo con las teclas (W,A,S,D). También es posible modificar la forma del terreno editando el mapa de bits ./res/terrain-heightmap.bmp (debe respetarse el tamaño del BMP).

Se incluyen las fuentes, aunque éstas tienen mucho más código del necesario y quizás marean más que ayudan a entender el asunto. Así que para cualquier duda no dudéis en contactar con nosotros!

No hay comentarios: