sábado, 15 de noviembre de 2008

¿Qué nos mantiene ocupados?

Pués estamos trabajando en un par de wrappers para ODE:
- ODE++
- OdeManager (for Irrlicht applications)
¿Suena bien? pues sigue leyendo.. :)


El primero, bastante demandado en la comunidad ODE, es un wrapper de ODE en C++. Se acabó ver ode como una colección de funciones aglomeradas en una librería. A través de éste sencillo wrapper pretendemos ordenar todas estas funciones en un conjunto de clases en C++ que nos faciliten su acceso. La principal ventaja que obtenemos es el poder navegar de forma ordenada entre todas estas funciones (ahora métodos :)) a través de la típica característica de autocompletar de los IDE actuales. Además de ahorrarnos el ir mareando de aquí p'allá con los ID de los objetos ODE.

El segundo ODE puede ser útil para todos aquellos que trabajen con Irrlicht como nosotros, ya que apoyándonos sobre el wrapper anterior, estamos implementando un "OdeManager" que encapsule las tareas cotidianas de toda aplicación Irrlicht que haga uso de ODE. Cuando la consideremos mínimamente aceptable la publicaremos, por el momento id abriendo boca con este preview de como sería una aplicación Irr+ODE con un par de esferas: una estática y una dinámica:


#include "irrlicht/irrlicht.h"
#include "OdeManager.h"
#include "IStaticSphereSceneNode.h"
#include "IDynamicSphereSceneNode.h"

using namespace ode;
using namespace irr;

int main()
{
IrrlichtDevice* device = createDevice(video::EDT_OPENGL, core::dimension2di(800,600),16,false);
ISceneManager* scene = device->getSceneManager();
scene->addCameraSceneNodeMaya();

OdeManager* odeMngr = createOdeManager( device );

IStaticSphereSceneNode* staticNode = odeMngr->mAddStaticSphereSceneNode( 10.0 );
IDynamicSphereSceneNode* dynamicNode = odeMngr->mAddDynamicSphereSceneNode( 10.0 );

dynamicNode->getGeom()->setPosition( 0, 25, 0 );
staticNode->getGeom()->setPosition( 0, -25, 0 );

while( device->run() )
{
odeMngr->mStep();

device->getVideoDriver()->beginScene(true, true, video::SColor(0,200,200,200));
device->getSceneManager()->drawAll();
device->getGUIEnvironment()->drawAll();
device->getVideoDriver()->endScene();
}

return 0;
}


Y recordad, un poco de feedback por vuestra parte siempre es agradecido!

Sigue leyendo >

jueves, 6 de noviembre de 2008

Tutorial 5: Irrlicht, ODE, os presento a irrKlang!

Siguiendo la línea de "Los Hola Mundos más Simples" pasamos a añadir a nuestra sencilla escena de la pelota una nueva librería para dotar a esta de sonido: irrKlang.
Librería, como podeis intuir, del mismo equipo que desarrolla Irrlich. Misma filosofía, misma simpleza, mismo potencial.


IrrKlang se distribuye como un proyecto aparte de Irrlicht, por ello tiene su propia web. También tiene diferencias en cuanto a su licencia, ya que si se desea utilizar para uso comercial hay que pagar por la versión Pro. Pero cabe destacar que la única diferencia entre la versión para uso NO comercial y la versión Pro es la posibilidad de compilar irrKlang en forma de librería estática. Como podeis observar en esta comparativa la versión no comercial mantiene las mismas interesantes características que la versión de pago.
Y al igual que irrlicht soportaba una gran cantidad de formatos de modelos 3D, irrklang tiene un buen soporte en formatos de audio (wav, ogg, mp3...). Aunque es importante recordar que MP3 tiene su propia licencia que se debería pagar aparte, es por ello que no está integrado realmente en el núcleo de la librería irrklang, sino que viene en forma de plugin ("ikpMP3.dll") por si te interesara eliminarlo de tu aplicación.
Para acabar esta introducción de la librería, comentar lo que me parece la característica más interesante: Sonido 3D. De forma graciosa el API de irrklang llama "Sonido 2D" al sonido digamos estéreo de toda la vida, y "Sonido 3D" a aquel que está posicionado en un espacio tridimensional y suena de una forma u otra dependiento de un ficticio observador (oyente en este caso ;D) de la escena. Y todo ésto es fácil de integrar en una escena irrlicht gracias al ISceneNode implementado para irrKlang que podemos encontrar en su web: irrKlangSceneNode.

Pero vayamos al grano. Un hola mundo de irrKlang se podría resumir en el siguiente par de líneas:

ISoundEngine* engine = createIrrKlangDevice();
engine->play2D("path/myFile", false);

Y ya tendríamos sonido en nuestra aplicación.
El booleano de play2D es por si queremos la reproducción en loop, así que para tener música de fondo tan sólo hay que ejecutar dicha línea con true.

En nuestro tutorial hemos dotado de sonido a la pelota cuando choca contra el suelo, y para ello hemos hecho uso de ese par de líneas, y además hemos ajustado el volumen según la fuerza del rebote para darle algo de gracia. Veamos como:

Un nuevo include:

#include <irrKlang/irrKlang.h>


Una nueva variable:

ISoundEngine* engine;


Una nueva inicialización de librería:

void irrklang_inicializacion()
{
engine = createIrrKlangDevice();
}


Y simplemente, en el instante en el que sabemos que se produce una colisión, reproducimos el sonido. Ésto es, dentro del "ode_nearCallback".
Primeros se ha capturado la velocidad lineal del body de la esfera mediante "dBodyGetLinearVel( sphereBody )", ODE siempre devuelve los datos en forma de array, como la velocidad lineal es un vector, esta función nos proporciona un array con (x,y,z). Sólo nos interesa la velocidad vertical, con lo cual: "dBodyGetLinearVel( sphereBody )[1]".
En las siguientes líneas simplemente hemos obtenido el valor absoluto de la velocidad lineal:

dReal verticalVelocity = dBodyGetLinearVel( sphereBody )[1];
if ( verticalVelocity < 0.0 )
verticalVelocity *= -1.0;


Y en el caso de que haya una velocidad lineal mínima en el eje Y, reproducimos el sonido del boing. Para dotar de algo más de realismo el sonido, modificamos el volumen mediante "setSoundVolume" en función de la velocidad. El volumen se ajusta de [0..1], así que simplemente, cuando la fuerza empiece a ser muy débil (<1.0) empazará a disminuir el volumen.

if( verticalVelocity > 0.1 )
{
engine->setSoundVolume( verticalVelocity );
engine->play2D("res/boing.mp3", false);
}


Y poco más!

A nivel de archivos, han sido añadidos al tutorial la dll "irrKlang.dll" y el pluging para mp3 "ikpMP3.dll", la librería estática "libirrKlang.a, libirrKlang.def" (tan sólo son la interfaz a la dll), y la colección de cabeceras en "include\irrKlang".

Ya sabeis, el código en el box bajo el nombre "tutorial_05.zip".

Sigue leyendo >

Completando el Cuarto Tutorial (Irrlicht+ODE): La Masa

Volvemos trás un pequeño letargo con novedades.
Pero antes que nada, retomemos el último tutorial publicado para añadirle un detalle que dejé en el tintero expresamente para evitar marear mucho más la perdiz con todas las nuevas líenas de código que ODE implicaba. Y aquello que ignoré fue la masa de la esfera, o dicho en términos ODE: la masa de un body.


Por la simpleza del tutorial, aplicar la masa no afecta demasiado en los resultados ya obtenidos sin ella, por eso decidí no mencionarla con tal de ganar un poco más en simplicidad de código (ya resulta bastante duro enfrentarse a una librería como ODE la primera vez, sin contar en la casi necesidad de estar mezclada con otra librería como Irrlicht).

Recordando ODE, teníamos Geoms y Bodys para representar la física de nuestros objetos. Los Geoms representaban su caracter geométrico en el espacio, y los bodys su facultad de movimiento. Y es por este movito por lo que son los bodys los encargados de cargar con la masa, ya que ésta se utiliza básicamente para ajustar el centro de gravedad de un objeto y su densidad. Ambas características afectan a la simulación del movimiento, y no a los cálculos de geometría y colisión.

Su aplicación es realmente sencilla. Retomando el último tutorial con nuestra pelotita rebotando contra el suelo, tan sólo hay que añadirle cuatro nuevas líneas de código:


dMass sphereMass;


Una nueva definición de variable. Ésta no es un identificador como lo eran todas las otras variables definidas para ODE, "dMass" es una pequeña estructura de datos que almacena la información nombrada referente a la masa: densidad, centro de gravedad, matriz de inercia y masa total.
Según ODE es un struct tal que así:


typedef struct dMass {
dReal mass; // total mass of the rigid body
dVector4 c; // center of gravity position in body frame (x,y,z)
dMatrix3 I; // 3x3 inertia tensor in body frame, about POR
} dMass;


Wow! ¿Y como se supone que voy a configurar yo esos valores??! Nada que temer, lo hará ODE por nosotros si trabajamos con primitivas geométricas.

El resto de líneas nuevas se encuentra en la función de creacion "ode_creación":


dMassSetSphere( &sphereMass, 1.0f, 10.0f);
dMassAdjust( &sphereMass, 0.25);

dBodySetMass( sphereBody, &sphereMass);


Las dos primeras líneas realizan toda la configuración del struct dMass que acabamos de ver. La primera recibe como parámetro la densidad deseada para nuestro objeto y su radio (por tratarse de una esfera, obviamente), y se encarga de configurar en el dMass las variables 'c' e 'I': el centro de gravedad y su matriz de inercia para nuestra esfera. Ésto se realiza gracias a la función "dMassSetSphere" que sabe como se ajustan dichos valores a un objeto esférico. Por supuesto, disponemos de funciones de ajuste de masa para todas las geometrías primitivas que ODE nos ofrecía (cajas, cilindros, esferas, cápsulas y trimesh).
Según ODE, éste método no ajusta la masa total del objeto (la variable 'mass' del struct dMass), dejando este parámetro ajustable por nosotros mediante la función "dMassAdjust".

Por último ya sólo queda asignar nuestra nueva masa configurada a su respectivo body mediante "dBodySetMass".

La ejecución del tutorial con los valores de masa que he puesto por defecto da un resultado similar al anterior, la pelota rebota prácticamente de la misma forma.
Ahora puedes probar a cambiarle la masa total mediante "dMassAdjust" a 1000.0 unidades por ejemplo, y observarás como nuestra pelota de playa empieza parecerse más bien a una bola de bolos.

Como de costumbre, el código fuente está disponible en el box baje el nombre "tutorial_04b.zip" ;)

Sigue leyendo >

sábado, 30 de agosto de 2008

Vehiculos con ODE (2ª parte)

Muy Buenas!! en la entrada anterior comentamos las modificaciones que le hicimos a un ejemplo de ODE para poder formar distintos tipos de vehiculos. Sin embargo, no pusimos codigo fuente alguno. Supongo que más de uno se quedaria con las ganas :*(, así que en esta entrega os vamos a exponer todo el codigo del ejemplo.


Antes de nada te recuerdo que esto es codigo exclusivamente ODE, no tiene nada de Irrlicht. Ahora mismo estamos trabajando en la integracion de este ejemplo con nuestra jerarquía de clases, de modo que todo el codigo que aquí verás quede hermosamente encapsulado en unas pocas clases. Bueno, vamos allá.

Para empezar, tenemos los includes de ode y drawstuff

#include "ode.h"
#include "drawstuff.h"
#include "texturepath.h"


A continuacion vemos la rista de definiciones que tuvimos que hacer para especificar todas las caracteristicas del vehiculo, tales como dimensiones globales de la carroceria, numero de ruedas, numero de cajas que componen el chasis, tracción, etc
#define LENGTH 1.75    // chassis length
#define WIDTH 0.7 // chassis width
#define HEIGHT 0.25 // chassis height
#define STARTZ 0.5 // starting height of chassis
#define CMASS 1.50 // chassis mass
#define WMASS 0.084 // wheel mass

#define NUM_WHEELS 6
#define NUM_DIRECTIONWHEELS 2
#define NUM_CHASSISBODIES 1
#define NUM_CHASSISGEOMS 3
#define NUM_BODIES (NUM_WHEELS + NUM_CHASSISBODIES)

//#define TRACCION_INICAL 0 // - - Delantera
#define TRACCION_INICAL NUM_DIRECTIONWHEELS // | | - Trasera
//#define TRACCION_FINAL NUM_DIRECTIONWHEELS // | - |
#define TRACCION_FINAL NUM_WHEELS // - Total -



Para especificar las dimensiones de cada caja y ruedas tenemos arrays de arrays, tanto para las dimensiones como para el posicionamiento relativo, radios y grosores de las ruedas, etc.
//Configuracion de las cajas para la Cabeza tractora de un Camion
dReal ChassisDim[3][3] =
{
{LENGTH,WIDTH,HEIGHT},
{LENGTH*0.5,WIDTH*0.5,HEIGHT},
{LENGTH*0.25,WIDTH*0.9,1.5*HEIGHT},
};

dReal ChassisPos[3][3] =
{
{0,0,0},
{0.55, 0, HEIGHT + HEIGHT*0.5 -0.1},
{0.25, 0,0.45}
};

dReal WheelsPos[6][3] =
{
{ 0.35*LENGTH, 0.45*WIDTH, STARTZ -0.5*HEIGHT},
{ 0.35*LENGTH, -0.45*WIDTH, STARTZ -0.5*HEIGHT},
{ -0.225*LENGTH, 0.4*WIDTH, STARTZ -0.5*HEIGHT},
{ -0.225*LENGTH, -0.4*WIDTH, STARTZ -0.5*HEIGHT},
{ -0.4*LENGTH, 0.4*WIDTH, STARTZ -0.5*HEIGHT},
{ -0.4*LENGTH, -0.4*WIDTH, STARTZ -0.5*HEIGHT}
};

#define RADIUS 0
#define GROSOR 1
dReal WheelsSize[6][2] =
{ //RADIUS GROSOR
{ 0.14, 0.1},
{ 0.14, 0.1},
{ 0.14, 0.2},
{ 0.14, 0.2},
{ 0.14, 0.2},
{ 0.14, 0.2},
};


Como ves, todas las medidas van en función de los defines WIDTH, HEIGHT Y LENGTH. Esto no quiere decir que tenga que ser estrictamente asi. De hecho, todos estos valores serán leidos de fichero.

A continuación tenemos el chorro de constantes mágicas de ODE para los contact joints de colisión y los joints hinge2 para las ruedas.
//Contantes para los ContactJoints
#define MAX_CONTACT_JOINTS 20
#define CONTACTJOIN_MU dInfinity
#define CONTACTJOIN_SLIP1 0.1
#define CONTACTJOIN_SLIP2 0.1
#define CONTACTJOIN_SOFT_ERP 0.5
#define CONTACTJOIN_SOFT_CFM 0.3

//Configuracion del Hinge2 de las ruedas
#define RUEDAS_MOTRICES_ParamFMax2 0.1
#define RUEDAS_FRENO_PROGRESIVO 0.01
#define STEER_MAX 1.0
#define HINGE2_DIRECTIONWHEELS_MAXDIFF_ANGLE 0.1
#define HINGE2_DIRECTIONWHEELS_MAXDIFF_SCALE 10.0
#define HINGE2_DIRECTIONWHEELS_ParamFMax 0.2
#define HINGE2_DIRECTIONWHEELS_ParamLoStop -0.75
#define HINGE2_DIRECTIONWHEELS_ParamHiStop 0.75
#define HINGE2_DIRECTIONWHEELS_ParamFudgeFactor 0.1
#define HINGE2_ParamSuspensionERP 0.4
#define HINGE2_ParamSuspensionCFM 0.8
#define HINGE2_FIXEDWHEELS_ParamLoStop 0
#define HINGE2_FIXEDWHEELS_ParamHiStop 0


//ODE World constants
#define ODE_STEP 0.05
#define ODE_GRAVITY -0.5



Hasta aqui los defines y arrays. En el siguiente bloque puedes ver la definición del mundo y el espacio raiz, el array de bodys y joints, etc., así como las dos variables controladas por el usuario, la velocidad y direccion del vehiculo.
// dynamics and collision objects (chassis, 3 wheels, environment)
static dWorldID world;
static dSpaceID space;
static dBodyID body[NUM_BODIES];
static dJointID joint[NUM_WHEELS]; // joint[0] and [1] are the front wheels
static dJointGroupID contactgroup;
static dGeomID ground;
static dSpaceID car_space;
static dGeomID box[NUM_CHASSISGEOMS];
static dGeomID sphere[NUM_WHEELS];
static dGeomID ground_box;



// things that the user controls

static dReal speed=0,steer=0; // user commands



Veamos ahora la función llamada por la simulación de ODE para gestion de las colisiones. Esta función es bastante similar a los otros ejemplos vistos de ODE.
// this is called by dSpaceCollide when two objects in space are
// potentially colliding.

static void nearCallback (void *data, dGeomID o1, dGeomID o2)
{
int i,n;

bool o1isSpace = dGeomIsSpace (o1);
bool o2isSpace = dGeomIsSpace (o1);
dSpaceID s1 = dGeomGetSpace (o1);
dSpaceID s2 = dGeomGetSpace (o2);
if(!o1isSpace && !o2isSpace && s1 == s2)
return;

// only collide things with the ground
// int g1 = (o1 == ground || o1 == ground_box);
// int g2 = (o2 == ground || o2 == ground_box);
// if (!(g1 ^ g2)) return;

dContact contact[MAX_CONTACT_JOINTS];
n = dCollide (o1,o2,MAX_CONTACT_JOINTS,&contact[0].geom,sizeof(dContact));
if (n > 0) {
for (i=0; i<n; i++) {
contact[i].surface.mode = dContactSlip1 | dContactSlip2 |
dContactSoftERP | dContactSoftCFM | dContactApprox1;
contact[i].surface.mu = CONTACTJOIN_MU;
contact[i].surface.slip1 = CONTACTJOIN_SLIP1;
contact[i].surface.slip2 = CONTACTJOIN_SLIP2;
contact[i].surface.soft_erp = CONTACTJOIN_SOFT_ERP;
contact[i].surface.soft_cfm = CONTACTJOIN_SOFT_CFM;
dJointID c = dJointCreateContact (world,contactgroup,&contact[i]);
dJointAttach (c,
dGeomGetBody(contact[i].geom.g1),
dGeomGetBody(contact[i].geom.g2));
}
}
}


Si observas el principio de la funcion veras que se realiza un pequeño testeo. "Si ninguno de los dos geoms son espacios y, ambos pertenecen al mismo espacio, no se realizará tratamiento de colision". Esto es para que el chasis de un coche no colisione con sus ruedas, pero que, por ejemplo dos coches puedan colisionar entre si, y por supuesto, un coche pueda interactuar con el suelo y otros objetos del mundo, como la rampa.

A continuación vemos las funciones llamadas por DrawStuff para la inicialización y el manejo del teclado. Estas funciones no interesan dado que en irrlicht son totalmente distintas.
// start simulation - set viewpoint

static void start()
{
dAllocateODEDataForThread(dAllocateMaskAll);

static float xyz[3] = {2.8317f,-3.9817f,3.8000f};
static float hpr[3] = {121.0000f,-27.5000f,0.0000f};
dsSetViewpoint (xyz,hpr);
printf ("Press:\t'a' to increase speed.\n"
"\t'z' to decrease speed.\n"
"\t',' to steer left.\n"
"\t'.' to steer right.\n"
"\t' ' to reset speed and steering.\n"
"\t'1' to save the current state to 'state.dif'.\n");
}


// called when a key pressed

static void command (int cmd)
{
switch (cmd)
{
case 'w': case 'W': speed += 0.3; break;
case 's': case 'S': speed -= 0.3; break;
case 'a': case 'A': steer -= 0.5; break;
case 'd': case 'D': steer += 0.5; break;
case ' ': speed = 0; steer = 0; break;
case '1':
{
FILE *f = fopen ("state.dif","wt");
if (f)
{
dWorldExportDIF (world,f,"");
fclose (f);
}
}
}
}



Veamos la funcion empleada por DrawStuff para el bucle de simulacion, simLoop.
static void simLoop (int pause)
{


Esta función tiene chicha. Por un lado, modificamos o actualizamos la velocidad de giro de las ruedas motrices, la dirección de las ruedas directrices (nótese que una rueda puede ser motriz y directriz a la vez, como en los 4x4, turismos de tracción delantera, etc.)
    if (!pause)
{
// motor
for(int i=TRACCION_INICAL; i<TRACCION_FINAL; ++i) //ahora mismo es 4x4
{
dJointSetHinge2Param (joint[i],dParamVel2,-speed);
dJointSetHinge2Param (joint[i],dParamFMax2,RUEDAS_MOTRICES_ParamFMax2);
}
if (speed > 0) speed -= RUEDAS_FRENO_PROGRESIVO;
else if (speed < 0) speed += RUEDAS_FRENO_PROGRESIVO;

// steering
for(int i=0; i<NUM_DIRECTIONWHEELS; ++i)
{
dReal v = steer - dJointGetHinge2Angle1 (joint[i]);
if (v > HINGE2_DIRECTIONWHEELS_MAXDIFF_ANGLE) v = HINGE2_DIRECTIONWHEELS_MAXDIFF_ANGLE;
if (v < -HINGE2_DIRECTIONWHEELS_MAXDIFF_ANGLE) v = -HINGE2_DIRECTIONWHEELS_MAXDIFF_ANGLE;
v *= HINGE2_DIRECTIONWHEELS_MAXDIFF_SCALE;
dJointSetHinge2Param (joint[i],dParamVel,v);
dJointSetHinge2Param (joint[i],dParamFMax,HINGE2_DIRECTIONWHEELS_ParamFMax);
dJointSetHinge2Param (joint[i],dParamLoStop,HINGE2_DIRECTIONWHEELS_ParamLoStop);
dJointSetHinge2Param (joint[i],dParamHiStop,HINGE2_DIRECTIONWHEELS_ParamHiStop);
dJointSetHinge2Param (joint[i],dParamFudgeFactor,HINGE2_DIRECTIONWHEELS_ParamFudgeFactor);
}
if (steer > STEER_MAX) steer = STEER_MAX;
if (steer < -STEER_MAX) steer = -STEER_MAX;

dSpaceCollide (space,0,&nearCallback);
dWorldStep (world,ODE_STEP);

// remove all contact joints
dJointGroupEmpty (contactgroup);
}


Y por otro es la encargada de pintar en pantalla el entorno, con funciones y objetos de DrawStuff. Te vuelvo a recordar que esta parte cambiará bastante cuando pasemos esto a irrlicht.
    dsSetColor (0,0.5,1);
dsSetTexture (DS_WOOD);

for (int i=0; i<NUM_CHASSISGEOMS; i++)
dsDrawBox (dGeomGetPosition(box[i]),dBodyGetRotation(body[0]),ChassisDim[i]);
dsSetColor (0.33,0.33,0.33);
for (int i=1; i<=NUM_WHEELS; i++)
dsDrawCylinder (dBodyGetPosition(body[i]),dBodyGetRotation(body[i]),WheelsSize[i-1][GROSOR],WheelsSize[i-1][RADIUS]);

dVector3 ss;
dGeomBoxGetLengths (ground_box,ss);
dsSetColor (1,1,1);
dsDrawBox (dGeomGetPosition(ground_box),dGeomGetRotation(ground_box),ss);
}


Pues bien, ahora viene cuando la matan y él está herido, aqui tenemos el main, encargado, en este ejemplo, de inicalizar ODE, DrawStuff, crear los geoms, bodys, joints y toda la pesca. Para empezar tenemos la creación e inicialización de DrawStuff, que no nos interesa, y la de ODE.
int main (int argc, char **argv)
{
dMass m;

// setup pointers to drawstuff callback functions
dsFunctions fn;
fn.version = DS_VERSION;
fn.start = &start;
fn.step = &simLoop;
fn.command = &command;
fn.stop = 0;
fn.path_to_textures = DRAWSTUFF_TEXTURE_PATH;
if(argc==2)
{
fn.path_to_textures = argv[1];
}

// Inicializacion ODE

dInitODE2(0);
world = dWorldCreate();
space = dHashSpaceCreate (0);
contactgroup = dJointGroupCreate (0);
dWorldSetGravity (world,0,0,ODE_GRAVITY);
ground = dCreatePlane (space,0,0,1,0);



Seguimos con la creación del chasis como un conjunto de cajas, en las que iterativamente vamos sumando las masa de cada caja y la traslada a la posición dada por el array de arrays que vimos al principio.
    dMass m2;
dMassSetZero (&m);
body[0] = dBodyCreate (world);
dBodySetPosition (body[0],0,0,STARTZ);

for (int k=0; k<NUM_CHASSISGEOMS; k++)
{
dMassSetZero (&m2);
box[k] = dCreateBox (0,ChassisDim[k][0],ChassisDim[k][1],ChassisDim[k][2]);
dMassSetBox (&m2,1,ChassisDim[k][0],ChassisDim[k][1],ChassisDim[k][2]);
dMassTranslate (&m2,ChassisPos[k][0],ChassisPos[k][1],ChassisPos[k][2]);
dMassAdd (&m,&m2);

dGeomSetBody (box[k],body[0]);
dGeomSetOffsetPosition(box[k],
ChassisPos[k][0]-m.c[0],
ChassisPos[k][1]-m.c[1],
ChassisPos[k][2]-m.c[2]);
}
// for (int k=0; k<NUM_CHASSISGEOMS; k++)
// {
// dGeomSetBody (box[k],body[0]);
// dGeomSetOffsetPosition(box[k],
// ChassisPos[k][0]-m.c[0],
// ChassisPos[k][1]-m.c[1],
// ChassisPos[k][2]-m.c[2]);
// }
dMassTranslate (&m,-m.c[0],-m.c[1],-m.c[2]);
dMassAdjust (&m,CMASS);
dBodySetMass (body[0],&m);



Seguimos con las ruedas. En este caso es mas simple (y no) puesto que cada rueda es un objeto simple, y no compuesto, como el chasis. Lo dificil puede estar en la rotación aplicada a las ruedas.
    // wheel bodies
for (int i=1; i<=NUM_WHEELS; i++)
{
body[i] = dBodyCreate (world);
dQuaternion q;
dQFromAxisAndAngle (q,1,0,0,M_PI*0.5);
dBodySetQuaternion (body[i],q);
dMassSetCylinder (&m, 1, 1,WheelsSize[i-1][RADIUS], WheelsSize[i-1][GROSOR]);
dMassAdjust (&m,WMASS);
dBodySetMass (body[i],&m);
sphere[i-1] = dCreateCylinder (0, WheelsSize[i-1][RADIUS], WheelsSize[i-1][GROSOR]);

dGeomSetBody (sphere[i-1],body[i]);
dBodySetPosition (body[i], WheelsPos[i-1][0],WheelsPos[i-1][1],WheelsPos[i-1][2]);
}



He aquí la parte importante del ejemplo, la creación de los joints de tipo hinge2, la configuración de la suspensión, simulada con los parametros de ERP y CFM y, la configuración de restricción de las ruedas traseras para que no puedan girar libremente como las de delante.
    // front and back wheel hinges
for (int i=0; i<NUM_WHEELS; i++)
{
joint[i] = dJointCreateHinge2 (world,0);
dJointAttach (joint[i],body[0],body[i+1]);
const dReal *a = dBodyGetPosition (body[i+1]);
dJointSetHinge2Anchor (joint[i],a[0],a[1],a[2]);
dJointSetHinge2Axis1 (joint[i],0,0,1);
dJointSetHinge2Axis2 (joint[i],0,1,0);
}

// set joint suspension
for (int i=0; i<NUM_WHEELS; i++)
{
dJointSetHinge2Param (joint[i],dParamSuspensionERP,HINGE2_ParamSuspensionERP);
dJointSetHinge2Param (joint[i],dParamSuspensionCFM,HINGE2_ParamSuspensionCFM);
}
// lock back wheels along the steering axis
for (int i=NUM_DIRECTIONWHEELS; i<NUM_WHEELS; i++)
{
dJointSetHinge2Param (joint[i],dParamLoStop,HINGE2_FIXEDWHEELS_ParamLoStop);
dJointSetHinge2Param (joint[i],dParamHiStop,HINGE2_FIXEDWHEELS_ParamHiStop);
}



Llegamos al último paso de la creación e inicialización. Creamos el espacio de colisiones del coche, carspace, le agregamos los geoms del chasis y las ruedas. Creamos también la rampa de salto como una caja inclinada.
    // create car space and add it to the top level space
car_space = dSimpleSpaceCreate (space);
dSpaceSetCleanup (car_space,0);
for(int i=0; i<NUM_CHASSISGEOMS; ++i)
dSpaceAdd (car_space,box[i]);
for(int i=0; i<NUM_WHEELS; ++i)
dSpaceAdd (car_space,sphere[i]);

// environment
ground_box = dCreateBox (space,2,1.5,1);
dMatrix3 R;
dRFromAxisAndAngle (R,0,1,0,-0.15);
dGeomSetPosition (ground_box,2,0,-0.34);
dGeomSetRotation (ground_box,R);



Inicializado todo el entorno, es hora de lanzar la simulacion con la llamada pertinente a la funcion de DrawStuff. Éste hara las llamadas oportunas a start, simLoop, y command. Obviamente, esta parte cambiará radicalmente en Irrlicht.
    dsSimulationLoop (argc,argv,352,288,&fn);



Para terminar, debemos dejar la cocina limpia y aseada, así que destruimos el chasis, las ruedas, el espacio global, el mundo y finalizamos ODE.
    //Destruccion del vehiculo
for(int i=0; i<NUM_CHASSISGEOMS; ++i)
dGeomDestroy (box[i]);
for(int i=0; i<NUM_WHEELS; ++i)
dGeomDestroy (sphere[i]);

//destruccion de ode
dJointGroupDestroy (contactgroup);
dSpaceDestroy (space);
dWorldDestroy (world);
dCloseODE();
return 0;
}



Hasta aquí todo el codigo del demo_buggy.cpp modificado. Si de verdad quieres compilar el codigo fuente bájate el paquetón de ODE aquí y reemplaza el ode-0.10.0\ode\demo\demo_buggy.cpp por el nuestro. Para compilarlo tendrás que ejecutar ./configure && make. Lamento no haber puesto en el zip un triste makefile ni las librerias.

En la próxima entrega abordaremos el tema de la integración de los vehiculos ODE e Irrlicht.

Sigue leyendo >

sábado, 16 de agosto de 2008

Vehiculos con ODE

Muy buenas otra vez, estos dias hemos estado bastante enmarronados comprendiendo la forma de trabajar que tiene ODE. Hace unos dias nos cogimos un par de ejemplos de ODE, entre ellos el demo_buggy (un pequeño cochecito de 3 ruedas que salta una rampa) para, a partir de él, llegar a construir la parte fisica de los vehiculos.

From Menus con Irr...




Podríamos utilizar el modelo 3D de cualquier coche y utilizarlo como malla de colisiones pero ODE desaconseja su uso debido a las perdidas de rendimiento (enorme), precisión y sobretodo estabilidad. Además, piénsalo detenidamente, cualquier modelo 3D de un vehiculo, personaje, etc. tiene cientos o miles de poligonos. Sin embargo, una caja como carroceria solo son 12 triangulicos.

Inicialmente pensamos en construir los vehiculos como una simple caja con 4 ruedas conectadas, pero empezamos a pensar en los camiones (mas ruedas), monovolumenes, tractores, autobuses, y, decidimos usar mas de una caja a fin de poder ajustar la forma fisica a la del modelo visual, pero aumentando minimamente la complejidad del modelo fisico.

El ejemplo de ODE, demo_buggy tiene tres ruedas, así que, evidentemente, lo primero que hicimos fue colocarle una cuarta y posicionarlas como es debido.

Lo siguiente que vimos fue que, el modelo fisico de las ruedas eran esferas, para mejorar el rendimiento. El problema que presentaba esto era que al pasar junto a un obstaculo, las esferas tropiezan con éste, con lo que las cambiamos por cilindros.

Despues fue el turno de las cajas. Para ello construimos un objeto compuesto por varias cajas. Esta parte nos llevó un poco más de tiempo puesto que el código de ejemplo de demo_boxstack de ODE es un poco críptico.

Ya solo restaba sustituir valores fijos por defines, matrices de medidas para las cajas, radios y anchura de las ruedas, etc. Tambien hemos dispuesto variables para cambiar facilmente el numero de ejes asi como de cajas.

Nuestra intención es implementar un editor de vehiculos para, de manera sencilla, poder añadir nuevos vehiculos, esto es, ajustando un modelo fisico a una caroceria. De momento nos entretuvimos en montar tres vehiculos de prueba.

El primero de ellos es un simple turismo a lo manovolumen.
From Menus con Irr...


El siguiente es la flagoneta de los malacatones.
From Menus con Irr...


Y por último, pero no por ello menos importante, como dijo Shia LaBeouf en Transformers: Señores, les presento a mi amigo: Optimus Prime.
From Menus con Irr...


Puedes bajarte los ejecutables aquí y probarlos tú mismo. las teclas de direccion del vehiculo son la cruz formada por las teclas WASD, salvo en el buggy original que sí que son las que indica la ventana del shell.

Ah! se me olvidaba. No os preocupeis por la falta de codigo, en breve lo postearemos ;). Es solo que estos ejemplos son puro codigo ODE y prefiero mostrarlo cuando lo tengamos integrado en el proyecto con Irrlicht.

Sigue leyendo >

viernes, 8 de agosto de 2008

Cuarto Tutorial: El HolaMundo más simple de ODE (con Irrlicht)

Reanudamos los tutoriales comenzando con la integración de ODE. Éste no es más que un motor de simulación física, por lo que necesitamos apoyarnos sobre un motor gráfico para visualizar dicha simulación. O dicho de otra forma, nos permite dotar de simulación física un entorno gráfico tridimensional.
Para empezar crearemos una pelota y la dejaremos caer para ver como rebota contra el suelo.


Como de costumbre, el código y los binarios se encuentran en el box: tutorial_04.zip. Hemos partido de una rápida refactorizacion en funciones del tutorial 3, dicha refactorización se encuentra también en el box bajo el nombre de tutorial_03b.zip por si el salto del tutorial 3 al 4 es algo confuso, ya que hemos duplicado el número de líneas de código al añadir ODE.
Aprovecharemos que el código está divido en funciones para realizar el análisis poco a poco.

Antes que nada, hagamos una pequeña introducción.

La idea básica es que vamos a tener por un lado un objeto poligonal esférico texturizado en Irrlicht que será nuestra pelota visible. Y por otro tendremos un objeto de colisión esférico en ODE que será nuestra pelota física y que ODE se encargará de animar en función de la gravedad configurada. Por lo tanto, la relación entre ambas librerías correrá por nuestra cuenta: crearemos ambos objetos y representaremos la pelota de Irrlicht en función de la posición en la que se encuentre la esfera física de ODE.
De esta forma conseguimos una animación física natural en nuestro entorno tridimensional totalmente automática.

Introduciendo un poco a ODE, hay que empezar dejando claro que es una librería de simulación de cuerpos rígidos, descartando otro tipo de cuerpos más complejos como elásticos, telas, fluidos, partículas, gases, etc. Al menos no de forma nativa, ya que quizás con un poco de ingenio uno puede recrear telas o la animación de algún tipo de partículas, así como simular superficies elásticas a través de la configuración de los puntos de colisión (dotando a estos con una fuerza de rebote peculiar) o incluso superficies blandas haciendo uso de los factores de corrección de la simulación permitiendo violar ligeramente el límite que una superficie de colisión impone.

Para conseguir todo esto ODE se basa en 2 tipos de objetos a los que llama "geom" y "body".
Para ODE un "geom" es el objeto geométrico que posee una forma determinada y colisiona con otros "geom". Tenemos a nuestra disposición una colección de primitivas geométricas de colisión (esfera, cilindro, plano, cubo..) para combinar a nuestro antojo, aunque de forma excepcional también podemos obtener una geometría de colisión directamente desde un modelo poligonal de malla (llamados "trimesh") a costa de un coste computacional de simulación muy alto (no recomendable).
Los "body" son aquellos objetos que se van a mover en nuestro mundo. No tienen una forma geométrica determinada (para eso ya están los "geom") pero si una masa y un centro de gravedad. Un body por lo tanto sufre el efecto de la gravedad y el de cualquier fuerza que se le aplique manualmente.

Dicho así tenemos los geoms como objetos estáticos de colisión y los body como objetos dinámicos de nuestro mundo. Entonces ¿como colisiona un body? ¿como animo un geom? La forma de trabajar con ODE es combinando ambos tipos como si de distintas características de nuestro objeto se tratara. Por ejemplo para representar los elementos que componen la escena de éste tutorial utilizaremos para el suelo un geom ya que éste nunca se va a mover, no va a ser más que un plano estático de colisión; y para recrear nuestra pelota necesitaremos por un lado un geom con forma esférica que colisione contra el suelo, y como además queremos que se mueva crearemos también un body con una masa determinada y un centro de gravedad correctamente situado. Como tanto el geom como el body van a representar nuestro mismo objeto (la pelota) deben ser vinculados para que "trabajen" al unísono, de esta forma nuestra pelota se moverá gracias a su body y colisionará gracias a su geom.

ODE define los geoms y los bodies como elementos de unas estructuras superiores sobre las que gestiona de forma óptima todos estos objetos que van a formar parte del mundo físico. Así los geoms son agrupados en los llamados "space", que son literalmente espacios de colisión: los geoms que pertenezcan a un mismo space colisionarán entre ellos (aunque la última palabra para que colisionen dos objetos la tenemos nosotros). Y por otro lado, todos los bodies cuyas nuevas posiciónes van a calcularse "simultáneamente" pertenecerán al mismo "world".

En resumen, vamos a tener geoms agrupados en spaces (que a su vez pueden contener otros space) y bodies agrupados en worlds.

El último concepto importante del universo ODE que queda por mencionar son los "joints". Un joint se puede definir como la "union" entre 2 objetos. Por ejemplo, el joint más básico es el "contac joint" que es el punto de contacto que se produce cada vez que 2 geoms colisionan. Pero la idea de los joints es mucho más avanzada ya que no solo define un punto de unión, sino como responde un objeto en relación a otro en función a unos límites de movimiento y grados de libertad que son definidos en el propio joint. De ésta forma podremos definir joints que respondan como una bisagra, como un pivote, como un pistón, etc.. Y al igual que los elementos anteriores, los joints pueden agruparse en estructuras más grandes llamadas "group joints".
Pero no nos asustemos por ahora con esto de los joints y demás que se queda grande para el tutorial que tenemos entre manos, así que vayamo al tema!


Antes de meternos con las funciones veamos la cabecera del código.

#include <irrlicht/irrlicht.h>
#include <ode/ode.h>

using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace gui;

IrrlichtDevice *device;
IVideoDriver* driver;
ISceneManager* smgr;
IGUIEnvironment* guienv;
ICameraSceneNode* cameraNode;
ISceneNode* sphereNode;

dWorldID world;
dSpaceID space;
dJointGroupID contactJointGroup;
dGeomID groundGeom;
dGeomID sphereGeom;
dBodyID sphereBody;

Simplemente un par de includes son necesarios para trabajar con Irrlicht y con ODE. Todos ellos estan incluidos en el tutorial por lo que no hay que instalar nada adicional en nuestra máquina. De igual forma, para poner en marcha una aplicación ODE tan solo necesitamos enlazar con una librería, también incluida en el tutorial.
Echa un vistazo al Makefile si quieres saber como compilar una aplicación con ODE.

A continuación los ya conocidos espacios de nombres de Irrlicht, pero ¿y los de ODE?
La librería ODE, internamente, está programada en C++, pero con tal de facilitar el acceso a su interfaz, su desarrollador decidió ofrecer su API en formato C, y por ese motivo no encontramos espacios de nombres. Así que lo único que necesitamos tener en cuenta para acceder con rapidez a las funciones y tipos de ODE es saber que todas ellas comienzan por 'd'.

Tras las variables globales de Irrlicht definidas (esta vez haremos uso simplemente de un nodo esfera) encontramos las de ODE, que como hemos dicho comienzan por dXXX.
Para cualquier simulación necesitamos como mínimo un mundo, un espacio de colisiones y un grupo de joints para los puntos de contacto. Para definir nuestra pelota física necesitamos un geom y un body. Y si queremos que colisione contra el suelo, éste debe ser definido de forma explicita mediante un geom (no necesita un body ya que va a ser estático).

Demos un vistazo rápido al main.

int main()
{
irrlicht_inicializacion();
ode_inicializacion();

irrlicht_creacion();
ode_creacion();

while( device->run() )
{
irrlicht_renderizacion();

ode_simulacion();
irrlicht_actualizacion();
}

irrlicht_finalizacion();
ode_finalizacion();

return 0;
}

Básicamente empezamos inicializando ambas librerías. A continuación creamos nuestros objetos irrlicht (cámara, pelota..) y nuestros objetos ODE (pelota, suelo..).
Una vez en el bucle de ejecución realizamos 3 pasos fundamentales: pintar el mundo irrlicht, dar un paso de simulación en el mundo ODE y actualizar el mundo irrlicht en función de los cálculos de ODE. En el momento que estos 3 pasos entran en ejecución constante su orden realmente es indiferente, pudiéndose colocar en cualquier otro al realizado en el tutorial.

Pasemos ahora a las funciones ODE, ya que el código Irrlicht no debería suponernos ninguna dificultad si hemos llegado hasta aquí pasando por los primeros tutoriales.

Empecemos por la inicialización.

void ode_inicializacion()
{
dInitODE();
world = dWorldCreate();
space = dSimpleSpaceCreate( NULL );
contactJointGroup = dJointGroupCreate( 0 );

dWorldSetGravity( world, 0.0, -0.01, 0.0 );
}

Se inicializa ODE y creamos las estructuras mencionadas: un world para nuestro body, un space para nuestros geoms y un jointGroup para nuestros joints de contacto que se generan en el instante de la colisión.
Además aprovechamos para definir una fuerza de gravedad en el mundo. Como queremos una fuerza clásica la hemos definido sobre el eje vertical Y y negativa hacia abajo.

Una pregunta interesante es ¿a que velocidad trabaja ODE? Y la respuesta es tan sencilla como compleja: a la máxima que le permita la máquina sobre la que se ejecuta. Al igual que al render no le hemos puesto ninguna limitación y se ejecuta al máximo frames por segundo que le sea posible, nuestra simulación física va a funcionar igual haciendo el mayor número de cálculos posibles, con la diferencia que si bien en el render no tiene demasiada importancia que los FPS aumenten en una máquina más potente en el caso de la simulación nos vamos a encontrar con que ésta es más rápida o más lenta según la CPU que la ejecute, cosa que no pinta nada bien.
En próximo tutoriales veremos como conseguir tiempos de simulación constantes independientes al procesador que utilicemos, pero por ahora simplemente ajusta a mano los valores del tutorial para tu máquina si no te gustan los resultados, como por ejemplo esta fuerza de gravedad que acabamos de definir.



void ode_creacion()
{
/* suelo */

groundGeom = dCreatePlane( space, 0, 1, 0, 0 );

/* pelota */

sphereGeom = dCreateSphere( space, 10.0f );
sphereBody = dBodyCreate( world );
dGeomSetBody( sphereGeom, sphereBody );

dBodySetPosition( sphereBody, 0, 100, 0 );
}

Como ya hemos comentado, creamos un geom para nuestro suelo. Los geom los creamos mediante las funciones "dCreateXXX". Dependiendo de la figura geométrica del geom elegiremos una función u otra. En este caso hemos utilizado la función del plano indicándole el space al que va a pertenecer el geom y la orientación de éste. El plano es definido directamente desde la ecuación paramétrica del mismo: a*X + b*Y + c*Z = d. Los parámetros de la función dCreatePlanet corresponden a los coeficientes "a,b,c,d", de esta forma, mediante "0,1,0,0" obtenemos un plano cuya normal se sitúa sobre el eje Y: un plano sobre los ejes XZ.

Acto seguido definimos la pelota mediante un geom esférico y un body. Como ambos elementos van a representar el mismo objeto los asociamos explicitamente.
Por último situamos la esfera en el lugar deseado. ¿dBodySetPosition ó dGeomSetPosition? Da igual, ambos están vinculados y al posicionar uno automáticamente posicionamos el otro.

Antes del extraño callback definido, veamos la función de simulación

void ode_simulacion()
{
dSpaceCollide( space, NULL, &ode_nearCallback );
dWorldStep( world, 0.05 );
dJointGroupEmpty( contactJointGroup );
}

3 nuevos pasos fundamentales.
El primero detecta posibles colisiones entre todos los geoms que pertenezcan al space indicado. Y digo posibles porque el callback pasado como parámetro (puntero a función) es invocado por ODE cada vez que los objetos están muy cerca (han intersectado sus bounding boxes pero no necesariamente las geometrías han colisionado). Dentro de dicho callback detectaremos y gestionaremos las colisiones reales.
Suponiendo que dicho callback ya se ha invocado para todas las colisiones de nuestro mundo, es el momento de actualizar la posición de todos los objetos del mundo en función de lo que haya sucedido en el paso anterior. (Este es otro valor importante que debes modificar a mano si la simulación en tu ordenador es muy rápida o muy lenta).
Y por último debemos eliminar todos los joints de contacto que se encuentren en el grupo. ¿¿Mande?? Como hemos dicho, un joint es una unión entre 2 objetos, y en las colisiones se produce un joint especial de contacto para poder configurar la colisión (fricción, rebote, etc..). Estos joints se generan en el primer paso de la simulación que acabamos de ver, en el segundo se procesan y en este tercero deben ser eliminados para que los objetos sigan su curso en la siguiente iteración.
Éste es el ciclo de vida de todo joint de contacto.

Veamos ahora los entresijos del callback mencionado.

void ode_nearCallback(void *userData, dGeomID geom1, dGeomID geom2)
{
const int maxPoints = 10;
dContact contactPoints[maxPoints];
int numPoints = dCollide( geom1, geom2, maxPoints, &contactPoints[0].geom, sizeof(dContact) );

if (numPoints > 0)
{
for (int i=0; i<numPoints; i++)
{
contactPoints[i].surface.mode = dContactBounce;
contactPoints[i].surface.bounce = 0.7;
contactPoints[i].surface.bounce_vel = 0.1;
dJointID contactJoint = dJointCreateContact( world, contactJointGroup, &contactPoints[i] );
dJointAttach( contactJoint, dGeomGetBody(contactPoints[i].geom.g1), dGeomGetBody(contactPoints[i].geom.g2));
}
}
}

Recibimos los 2 geoms que están en posible colisión. Para resolver la situación invocamos dCollide: función que detectará la colisión con detalle y calculará el número de puntos en los que ambos geoms colisionan.
Si no hay colisión, sencillamente obtendremos una cantidad de 0 puntos de contacto entre ambos geoms. Pero si se ha calculado algún punto de contacto habrá que configurarlo mediante la estructura definida "dContact". En este tutorial vamos a simular una superficie elástica para conseguir que la pelota rebote, para ello debemos modificar 2 parámetros de la superficie del punto de contacto: "bounce" y "bounce_vel". Asignamos unos valores para el rebote y creamos un joint a partir de dicha configuración. Por último solo tenemos que asociar el joint creado con los dos geoms que han entrado en juego en la colisión.


void ode_finalizacion()
{
dBodyDestroy( sphereBody );
dGeomDestroy( sphereGeom );
dGeomDestroy( groundGeom );
dJointGroupDestroy( contactJointGroup );
dSpaceDestroy( space );
dWorldDestroy( world );
dCloseODE();
}

Por último sencillamente liberamos uno a uno los distintos elementos que hemos ido creando en orden inverso.

Antes de finalizar, y una vez que hemos visto como funciona ODE, revisamos la función de irrlicht de actualización.

void irrlicht_actualizacion()
{
dReal* odePosition = (dReal*)dGeomGetPosition( sphereGeom );
vector3df newIrrlichtPosition;
newIrrlichtPosition.set( (f32)odePosition[0], (f32)odePosition[1], (f32)odePosition[2] );
sphereNode->setPosition( newIrrlichtPosition );
}

La pelota de Irrlicht no es más que el render final animado gracias a la física calculada por ODE, por lo tanto la posición de la pelota de Irrlicht debe ser actualizada de forma automática, y esto es gracias a esta función.
Simplemente obtenemos la posición actual del geom de la esfera de ODE (podríamos coger perfectamente la posición del body, ya que no hay diferencia como ya hemos comentado) y se la aplicamos a irrlicht. Como cada librería utiliza estructuras diferentes, sencillamente hay que transformar de una estructura a otra y aplicar al nodo esfera.

Sigue leyendo >

martes, 1 de julio de 2008

ODE 0.10

Julio viene calentito! Se acaba de lanzar una nueva versión de nuestro motor físico preferido: ODE 0.10
Para ver el changelog sigue leyendo.


Changes for 0.10:

* New functions: dInitODE2(), dAllocateODEDataForThread(),
dCleanupODEAllDataForThread(). This adds support for ability to run
collision detection from multiple threads for separate spaces.
* New functions: dCheckConfiguration(), dGetConfiguration(), to test
how ODE was compiled.
* New function: dJointGetNumBodies (patch #1901550).
* New function: dSpaceGetClass (patch #1901637).
* New function: dSetColliderOverride(), to register custom collision
handling functions.
* Added damping and MaxAngularVel() functions.
* Applied patch #1335202: Contact Joint Motion (see demo_motion.cpp).
* Applied patch #1851394: support for GIMPACT with double precision,
dCollide fix.
* Applied patch #1813079 (moved callback).
* Added possibility to collide a space of lower sublevel as a
geometry against another space of a higher level with dSpaceCollide2.
dSpaceSetSublevel/dSpaceGetSublevel are used for sublevel
assignment/retrieval.
* Fixed a bug in dJointXXXGetInfo. The value in limot.limit was not
always updated. (Ex: If hi and lo limit were changed).
* New Joint: Prismatic Universal (patch #1828454).
* New Joint: Piston, with demo.
* Disabled building shared library by default with autotools. ODE
shouldn't be installed as a system library.
* Fixed drawstuff build issues on OSX.
* Removed release and debug flags for configure.in: CPPFLAGS,
CFLAGS, CXXFLAGS should be set by the user to their liking, respecting
autotools policies.
* Added 'Sweep and Prune' collision space.
* Optimizations, many bug fixes, and code cleanup.

Sigue leyendo >

irrKlang se actualiza

Se acaba de anunciar la salida de irrKlang 1.1.0, lo que parece un pequeño gran salto desde la anterior versión 1.0.4.
Según en changelog, en resumen aporta optimizaciones de velocidad, mejor uso de la memoria y algunos bug fixes.

Por otro lado, también se ha actualizado el IrrklangSceneNode para su integración con la versión 1.4 de Irrlicht

Sigue leyendo >

martes, 17 de junio de 2008

Tercer tutorial: el HolaMundo más simple de Irrlicht (3ª parte)

Finalizamos esta primera ronda de mini-tutoriales alcanzando al fin el tutorial 1 de Irrlicht.
Lo visto hasta ahora nos servirá de base para los próximos tutoriales en los que iremos viendo como dotar de simulación física nuestra aplicación mediante el motor ODE, y otros conceptos más avanzados de Irrlicht a medida que los vamos aprendiendo.




#include <irrlicht/irrlicht.h>

using namespace irr;

using namespace core;
using namespace scene;
using namespace video;
using namespace gui;

int main()
{
/** INICIALIZACION **/

IrrlichtDevice *device = createDevice(
video::EDT_OPENGL,
dimension2d(640, 480), 16,
false, false, false, NULL );

IVideoDriver* driver = device->getVideoDriver();
ISceneManager* smgr = device->getSceneManager();
IGUIEnvironment* guienv = device->getGUIEnvironment();


device->setWindowCaption(L"Hola Mundo - Tutorial 3");

guienv->addStaticText(L"Esto es texto fijo insertado en el GUI de la aplicacion",
rect(10,10,260,22), true);

/** CREACION **/

ICameraSceneNode* cameraNode = smgr->addCameraSceneNodeFPS( NULL, 100.0f, 200.0f );
IAnimatedMesh* dwarfMesh = smgr->getMesh( "res/dwarf.x" );
IAnimatedMesh* faerieMesh = smgr->getMesh( "res/faerie.md2" );
IAnimatedMeshSceneNode* dwarfNode = smgr->addAnimatedMeshSceneNode( dwarfMesh );
IAnimatedMeshSceneNode* faerieNode = smgr->addAnimatedMeshSceneNode( faerieMesh );
dwarfMesh->drop();
faerieMesh->drop();

/** ACTUALIZACION **/

dwarfNode->setScale( vector3d(.5f,.5f,.5f) );
dwarfNode->setPosition( vector3df(25,0,0) );
dwarfNode->setMaterialFlag( EMF_LIGHTING, false );
faerieNode->setPosition( vector3df(-25,15,0) );
faerieNode->setMaterialFlag( EMF_LIGHTING, false );
faerieNode->setMD2Animation ( scene::EMAT_STAND );
faerieNode->setMaterialTexture( 0, driver->getTexture("res/faerie.bmp") );
cameraNode->setPosition( vector3df(40,30,-50) );
cameraNode->setTarget( vector3df(0,5,0) );

/** EJECUCION **/

while( device->run() )
{
driver->beginScene( true, true, SColor(255,100,101,140) );
smgr->drawAll();
guienv->drawAll();
driver->endScene();
}

/** FINALIZACION **/

device->drop();
return 0;
}


Primera novedad: un nuevo objeto extraído del dispositivo Irrlicht principal: GUIEnvironment. Hasta ahora habíamos visto Irrlicht únicamente como un motor gráfico 3D, pero nada más lejos de la realidad. Nuestro engine favorito nos ofrece toda una colección de componentes para implementar interfaces de usuario 2D integradas perfectamente con el entorno tridimensional. Al igual que el SceneManager lo utilizamos para gestionar la escena 3D, disponemos del GUIEnvironment para gestionar los widgets (como les llaman otros APIs) de nuestra aplicación. Tenemos a nuestra disposición: button, checkbox, menu, file dialog, tab, window, scrollbar, etc. Como si de una aplicación de escritorio se tratara, ideal para diseñar nuestros menús en la propia aplicación. Como curiosidad, hay incluso quién ha utilizado Irrlicht exclusivamente como librería GUI para desarrollar alguna aplicación de escritorio.

Un sencillo ejemplo lo tenemos en las instrucciones siguientes, en el que añadimos un mensaje de texto fijo y visible sobre nuestra escena. Además de la cadena de caracteres, se le especifica el tamaño y posición del área rectangular del texto a través del objeto rect. El booleano es para indicar si queremos el área con o sin borde. (Si es la primera vez que ves esa "L" delante de una cadena de caracteres, busca información sobre el uso de wide chars en C/C++)

También le hemos cambiado el título a la ventana a través del device.

En este tutorial hemos sustituido la caja por modelos más complejos como los que utilizaríamos en una aplicación real. Realizamos la carga como ya vimos, con la diferencia de que ahora utilizamos un puntero de tipo "AnimatedMesh" ya que sabemos de antemano que los modelos que vamos a utilizar están animados. Luego los insertamos en la escena también como nodos de malla animada. Y por último liberamos las mallas.

Los modelos utilizados, además de ser más complejos poligonalmente están texturizados. En el modelo "dwarf", el propio formato DirecX (.x) incluye una referencia a la textura utilizada e Irrlicht la carga y aplica automáticamente. En cambio, el modelo "faerie" que está en formato MD2(Quake2) y éste no incluye la referencia a la textura por lo que debemos cargarla y asignarla nosotros manualmente. De esta forma, utilizando el VideoDriver podemos cargar imágenes y transformarlas al formato interno: Texture. Esta nueva textura puede ser aplicada al modelo a través del método "setMaterialTexture" de su nodo.
Como algo excepcional, el formato MD2 contiene un conjunto de animaciones pre-definidas propias del Quake2 (éstas son acciones del personaje: andar, disparar, agacharse, saltar, morir, etc.). Y es posible seleccionar rápidamente una de ellas utilizando el método específico del formato "setMD2Animation" pasándole alguna de las constantes: irr::scene::EMAT_* (Enum Md2 Animation Type).
Por último, como en este tutorial no hemos llegado a tocar el tema de las luces, el material de los modelos (su textura aplicada en este tutorial) se renderizará muy oscuro a causa de la ausencia de iluminación. Una solución rápida es desactivar el efecto de la iluminación sobre el material, y eso se hace a través del conjunto de flags de un material: setMaterialFlag( irr::video::EMF_*, true|false ). Curiosea sobre el conjunto de flags para ver de cuentas maneras puedes modificar el acabado de un material.

Ya solo resta añadir en el bucle de ejecución la pintada de los elementos de interfaces de usuario, y es tan fácil como decirle "drawAll" al GUIEnvironment.
Pon especial atención al orden de los "drawAll" del GUI y del SceneManager, ya que si invirtiéramos su orden estaríamos pintando los modelos encima del mensaje de texto, y a menos que sea el efecto que buscamos provocará extraños resultados.

Sigue leyendo >

lunes, 16 de junio de 2008

Segundo tutorial: el HolaMundo más simple de Irrlicht (2ª parte)

Seguimos con el segundo tutorial ampliando ligeramente la versión del HolaMundo realizada en la primera parte, sin correr demasiado para ir asimilando bien las bases de Irrlicht.


De nuevo, los fuentes y el binario ya compilado están en el box: tutorial_02.zip.

El código sin comentarios quedaría tal que así:

#include <irrlicht/irrlicht.h>

using namespace irr;

using namespace core;
using namespace scene;
using namespace video;

int main()
{
/** INICIALIZACION **/

IrrlichtDevice *device = createDevice(
video::EDT_OPENGL, dimension2d(640, 480), 16,
false, false, false, NULL );

IVideoDriver* driver = device->getVideoDriver();
ISceneManager* smgr = device->getSceneManager();

/** CREACION **/

ICameraSceneNode* cameraNode = smgr->addCameraSceneNodeFPS( 0, 100.0f, 200.0f );

IMesh* boxMesh = smgr->getMesh( "res/box.x" );
IMeshSceneNode* boxNode = smgr->addMeshSceneNode( boxMesh );
boxMesh->drop();

/** ACTUALIZACION **/

boxNode->setScale( vector3d(10.0f,10.0f,10.0f) );
cameraNode->setPosition( vector3df(20,30,-40) );
cameraNode->setTarget( vector3df(0,5,0) );

/** EJECUCION **/

while( device->run() )
{
driver->beginScene( true, true, SColor(255,100,101,140) );
smgr->drawAll();
driver->endScene();
}

/** FINALIZACION **/

device->drop();
return 0;
}


Un primer cambio es apreciable en la cámara. El SceneManager de Irrlicht contiene un par de cámaras más sofisticadas que la utilizada en el primer tutorial. Estas cámaras destacan principalmente por venir pre-configuradas con eventos de teclado y ratón asociadas a ellas permitiendo manipularlas de forma intuitiva sin ninguna línea de código adicional. Las cámaras son llamadas "FPS" y "Maya". Si eres un jugón o te gusta el diseño gráfico ya te estarás imaginando el tipo de cámaras de las que hablamos.
La cámara FPS imita la configuración de la cámara típica de un Shooter (First Person Shooter) asociando los cursores a un movimiento de traslación horizontal y el ratón a la rotación de la cámara. Eso si, no esperes ningún tipo de colisión por defecto, ni siquiera con el plano horizontal XZ (el "suelo"), imagina más bien la configuración de cámara de un observador del shooter que está en "modo vuelo".
La cámara de tipo Maya tiene una configuración típica de un editor 3D, en el que la pulsación de los distintos botones del ratón y el movimiento del mismo activan las distintas opciones de: traslación, rotación y escalado/zoom de la cámara.
Ambas cámaras aceptan los mismos parámetros, y que interesen hasta el momento son el segundo y el tercero: velocidad de rotación y velocidad de traslación respectivamente.

Pero el verdadero cambio llega en las siguientes instrucciones. En este tutorial no vamos a pintar nosotros la caja explícitamente, en lugar de eso hemos abierto el Blender y la caja que aparece por defecto ha sido exportada a un formato que entienda Irrlicht, como por ejemplo DirectX (ficheros .x en texto plano). En cualquier aplicación gráfica que hagamos vamos a echar mano de modelos pre-diseñados, así que es el momento de enfocar el tutorial en ellos. El primer paso es realizar la carga del modelo y para ello se utiliza el método getMesh del SceneManager que es el encargado de generar una malla de Irrlicht a partir de un objeto almacenado en un archivo. Como se puede observar, tan solo se le proporciona como parámetro el modelo deseado, e Irrlicht internamente utiliza el cargador específico del formato en función de la extensión del fichero. Si le pasáramos una extensión que desconoce o sufriera algún tipo de error en la carga nos los indicará por la salida de consola.
Con nuestro modelo en forma de "Mesh", es el momento de añadirlo a nuestra escena de la misma forma que añadimos la cámara a traves del SceneManager. Hecho esto, la malla ya no la necesitamos y podemos hacer "drop" sobre ella. A partir de ahora, nuestra caja está encapsulada en un objeto llamado "MeshSceneNode" que nos va a proporcionar métodos para su manipulación, lo cual va a ser una gran ventaja comparado al tutorial anterior en el que la caja no la teníamos empaquetada de ninguna forma y nos dedicábamos a re-crearla en tiempo de render. ¿Porqué eso de "SceneNode"? El concepto de "nodo" en Irrlicht es la piedra angular de todo el engine. Ya comentábamos que el elemento SceneManager era la raíz de toda nuestra escena, pues bien, esto hay que tomárselo al pie de la letra ya que la escena está internamente representada en forma de árbol lógico con lo que nuestro SceneManager es literalmente la raíz del árbol. Partiendo de esta idea, es obvio entender como todo aquello que sea añadido a la escena es de forma genérica un nodo o "SceneNode". Iremos poco a poco descubriendo distintas implementaciones de nodos que extienden el SceneNode genérico para tratar con objetos más específicos, como por ejemplo los vistos hasta ahora: CameraSceneNode y MeshSceneNode.

En las últimas nuevas líneas de este tutorial podemos observar como realizar operaciones simples sobre nuestros objetos puestos en escena (la cámara y la caja). A través del puntero al nodo que hemos conservado se ha ajustado el escalado de la caja y la posición y enfoque de la cámara. Y lo mejor de todo, la tarea de render es poco más que un "drawAll" de nuestro gestor de escena.

Para concluir el tutorial, comentar que lo que ha sucedido internamente (respecto al tutorial 1) es que el pintado de la caja ha sido abstraído dentro del SceneNode, ya que es uno de sus método el encargado de acceder al VideoDriver para pintar la caja de forma similar a como lo hicimos en el tutorial anterior, y éste método es invocado automáticamente a través del "drawAll". Con lo cual, si estas pensando en hacer pintadas de forma manual a bajo nivel utilizando directamente el VideoDriver, te interesa implementarte tu propio SceneNode: tan sencillo como heredar de la interfaz "ISceneNode" e implementar sus métodos. Así integrarás limpiamente tu aplicación con Irrlicht.

Sigue leyendo >

sábado, 14 de junio de 2008

Primer tutorial: el HolaMundo más simple de Irrlicht

Nos ponemos las pilas comenzando con una colección de tutoriales que partan desde lo más básico. Empezamos obviamente con Irrlicht, con un HolaMundo más simple si es posible que el tutorial HelloWorld que acompaña al engine.


Antes que nada, el código y demás está disponible en el box de nuestro blog bajo el nombre de "tutorial_01". Su puesta en marcha debería ser tan sencilla como el tener instalado MinGW en el sistema, y ejecutar make sobre el tutorial, ya que han sido incluidas las cabeceras y librerías de Irrlicht necesarias para funcionar, haciendo así el código más portable y sencillo de poner en marcha al reducir los pre-requisitos del sistema en el que vaya a ser compilado/ejecutado.
Queda pendiente añadirle las librerías de linux y hacerlo así multiplataforma, por ahora sólo incluye lo necesario para ponerlo en marcha bajo MsWindows con MinGW.

Este HolaMundo ha sido pensado para aquellos que vienen (o venimos) de librerías gráficas de bajo nivel como OpenGL o DirectX y estamos acostumbrados a realizar el pintado por pantalla casi de forma manual, pensando siempre en vértices, aristas, normales, etc.. Así que apartaremos de nuestra vista aquellos objetos de Irrlicht más avanzados intentando dejar el mínimo número posible de objetos necesarios.

Empecemos echándole un ojo al código, y luego lo comentamos.
(Se han eliminado los comentarios que el zip del tutorial sí incluye)

#include <irrlicht/irrlicht.h>

using namespace irr;
using namespace core;
using namespace scene;
using namespace video;

int main()
{
IrrlichtDevice *device = createDevice(
video::EDT_OPENGL,
dimension2d(640, 480), 16,
false, false, false, NULL );

IVideoDriver* driver = device->getVideoDriver();
ISceneManager* smgr = device->getSceneManager();

smgr->addCameraSceneNode( NULL, vector3df(20,30,-40), vector3df(0,5,0) );

while( device->run() )
{
driver->beginScene( true, true, SColor(255,100,101,140) );
smgr->drawAll();
driver->draw3DBox( aabbox3df(0.f,0.f,0.f, 10.f,10.f,10.f) );
driver->endScene();
}

device->drop();
return 0;
}


Lo primero que se observa es que para trabajar con Irrlicht tan solo hay que incluir una cabecera (que por cierto el blog por el momento no visualiza al confundirlo con una etiqueta HTML). Y lo mismo pasa a nivel de linkado, si observas el Makefile verás que tan solo ha habido que enlazar con una librería.

Todo el API de Irrlicht se encuentra bajo el espacio de nombres "irr". Y está estructurado mediante 5 sub-espacios de nombres más: core, scene, video, io, gui.

La primera instrucción que no puede faltar en una aplicación Irrlicht es una llamada a "irr::createDevice". Ésta función en la encargada de inicializar todo el motor gráfico, representado mediante el objeto "IrrlichtDevice" devuelto. Éste objeto es la madre del cordero de toda aplicación Irrlicht.
Vemos que en la incialización estamos definiendo la ventana de render a través del conjunto de parámetros que tiene la llamada:

- "deviceType": el primero de ellos nos permite seleccionar el motor de render utilizado en la aplicación. Irrlicht incluye una buena colección: OpenGL, DirectX8, DirectX9 y 2 implementaciones software propias. No hay excusas para no hacer aplicaciones multiplataforma! Los identificadores de los distintos motores los podemos encontrar en irr::video::EDT_*. "EDT" significa "Enum Device Type", y es muy común encontrarse este tipo de nomenclatura en el API, así que no lo olvides para encontrar por ti mismo aquello que estés buscando en un futuro.

- "windowSize": el tamaño de la ventana de render, como su nombre indica, expresado en pixels como suele ser costumbre. Aquí se espera un objeto irr::core::dimension2d que encapsula el par de valores.

- "bits": la profundidad de color en bits de la ventana. Típicamente "16" o "32". No olvidar que este valor sólo tiene efecto al trabajar a pantalla completa, ya que de otra forma se utiliza el valor con el que tengas configurado el escritorio.

- "fullscreen": un booleano que especifica si queremos la aplicacion a pantalla completa o no.

- "stencilbuffer": mediante un booleano indicamos si queremos hacer uso del "stencilbuffer" para sombras. Un concepto algo avanzado para el objetivo de este primer tutorial.

- "vsync": activamos o desactivamos la sincronización vertical de nuestra tarjeta gráfica. De nuevo solo funcionaría al trabajar a pantalla completa.

- "eventReceiver": un objeto de la clase "EventReceiver" encargado de gestionar todos los eventos que queremos capturar en nuestra aplicación. Ya llegaremos..


Tras crear nuestro dispositivo Irrlicht, extraemos de él dos de los objetos más importante que tiene instanciados. Como son usados frecuentemente, es habitual hacer con una referencia propia para no estar continuamente haciendo "device->getXXX()".
Los nombres de ambos objetos son bastante intuitivos: VideoDriver y SceneManager. A groso modo podríamos decir que VideoDriver trabaja a bajo nivel y SceneManager nos proporciona la capa más alta de abstracción de todo el motor gráfico.
Ésto mismo lo comprobamos con la siguiente instrucción en la que añadimos una cámara a nuestra escena mediante "addCameraSceneNode". Sus parámetros son la posición de la cámara y el punto al que mira, y para encapsular estos valores se utiliza la clase irr::core::vector3d.

El siguiente paso es entrar de lleno en el bucle de ejecución de nuestra aplicación gráfica. La condición del bucle puede ser tan sencilla como "device->run()". En este caso el cerrar la ventana o la aplicación detendrá el dispositivo irrlicht y provocará que finalice el bucle.
El bucle estará gobernado por dos instrucciones al más puro estilo OpenGL ó máquina de estados: "beginScene" y "endScene". El objetivo de "beginScene" es básicamente limpiar la pantalla, y para ello le pasamos el color con el que queremos que deje el fondo utilizando la clase irr::video::SColor( alpha, r,g,b ). Entre ambos métodos colocaremos todas aquellas instrucciones que tengan como objetivo pintar cosas en pantalla. Por ese motivo encontramos en este primer tutorial una llamada al drawAll del SceneManager, el cual se encarga el solito de pintar todo aquello que le hayamos añadido, en nuestro caso tan solo ha sido la cámara así que la "colocará" en escena.
A continuación probamos hacer una pintada nosotros mismos echando mano directamente del driver de video para pintar un cubo. Si curioseamos las llamadas drawXXX del videoDriver observaremos como son primitivas: triángulos, lineas, cajas/cubos, vértices, etc. El objeto utilizado para definir el cubo no es más que una clase que almacena 2 puntos (x,y,z, X,Y,Z). La nomenclatura "aabbox" viene del término utilizado habitualmente para referirse a los boundingbox: "aabb".
Finalmente tenemos la llamada a endScene que es la encargada realmente de hacer el render por pantalla, ya que antes estábamos pintando sobre un buffer distinto.

Por último, solo queda destruir el device en el momento de cerrar la aplicación, y en el API de Irrlicht todos los objetos siempre de liberan/destruyen mediante un método común llamado "drop".

Y hasta aquí el tutorial.
En el próximo nos acercaremos un poco más al HelloWorld "oficial" de Irrlicht para entender cual es la filosofía a seguir del motor para pintar las cosas, ya que veremos que la idea es no utilizar el driver directamente, que para algo existe la abstracción del SceneManager.

Sigue leyendo >

jueves, 5 de junio de 2008

Irrlicht 1.4.1

¡Irrlicht se actualiza! 5 meses después de la versión 1.4.0.
También lo ha hecho IrrEdit actualizándose con la nueva versión de Irrlicht.

Irrlicht 1.4.1 released

We just released Irrlicht 1.4.1, it is now available at the sourceforge servers. Happy downloading. Version 1.4.1 is mainly a bugfix release and includes:

* Improvements to the COLLADA 1.3 loader, support for Milkshape 1.8 files, Enhanced .3ds, .obj loaders, Microsoft .x file animation playback improvements
* Added X11 support on OSX
* Several speed optimizations
* Tons of bugfixes, API enhancements and other small improvments

Sigue leyendo >

lunes, 26 de mayo de 2008

Un ejemplo de IApplication

Con este artículo concluimos el bloque de la estructura de menus Irrlicht. Sin más preámbulos, vamos a ver el código y clases necesarias para tener una aplicación basica para nuestro proyecto.


Empezamos con el main, en la que instanciamos un objeto de tipo LSim.
#include <iostream>
#include "LSim.h"

using namespace std;

int main()
{
LSim *app = new LSim();
return app->mRun();
}


Yo no se vosotros, pero adoro los mains así de limpitos ;). La clase LSim es la encargada de instanciar el IrrlichtDevice. A su vez, instancia un objeto MainApplication (el menú principal) pasándole al constructor el device. A continuación vemos la declaracion de LSim.

class LSim
{
public:
LSim();
virtual ~LSim();
bool mRun();

protected methods:
bool mInit();
// bool Loop();

protected:
IrrlichtDevice *f_device;
MainApplication *mainApp;
};


Y su implementación. No pongo el constructor y destructor porque estan vacios.

bool
LSim::mRun()
{
if(!mInit()) return false;

mainApp = new MainApplication(f_device);
mainApp->mRun();
return true;
}

bool
LSim::mInit()
{
try
{
// create device and exit if creation failed
dimension2d<s32> resolucion(640,480);
bool fullScreen = false;
u32 bpp = 32;
f_device = createDevice(video::EDT_OPENGL, resolucion,bpp,fullScreen);
if (f_device == NULL) return false;
return true;
}
catch(std::exception &e)
{
std::cerr<<"Error: "<<e.what()<<std::endl;
}
return false;
}


Acabamos de ver cómo se llama a MainApplication. Veamos ahora la declaración de MainApplication.

class MainApplication : public IApplication
{
public:
MainApplication(IrrlichtDevice *device);
virtual ~MainApplication();
virtual bool mInit();
virtual bool mRemove();

//events
static void mButtonQuit_Click(void* o, const SEvent& event);
static void mButtonNewWindow_Click(void *o, const SEvent & event);
static void mButtonFileOpen_Click(void *o, const SEvent & event);
static void mScrollBar_Changed(void *o, const SEvent & event);

// static void mSubButton_Click(void *o, const SEvent & event);

static void mButtonAddLogo_Click(void* o, const SEvent& event);
static void mButtonRemoveLogo_Click(void *o, const SEvent & event);
static void mButtonLaunchEditor_Click(void *o, const SEvent & event);
static void mButtonLaunchSimulator_Click(void *o, const SEvent & event);

protected:
EditorApplication *f_editor;
SimulatorApplication *f_simulador;

//controls
IGUIContextMenu * f_menu;
IGUIButton *f_buttonQuit;
IGUIButton *f_buttonNewWindow;
IGUIButton *f_buttonFileOpen;
IGUIButton *f_buttonAddLogo;
IGUIButton *f_buttonRemoveLogo;
IGUIButton *f_buttonLaunchEditor;
IGUIButton *f_buttonLaunchSimulator;
IGUIStaticText *f_staticTextTransparentControl;
IGUIScrollBar *f_scrollbar;
IGUIStaticText *f_staticTextLogger;
IGUIListBox *f_listBoxLogger;
IGUISkin* f_skin;
IGUIFont* f_font;
IGUIImage* f_ImageLogo;
IGUIStaticText *f_staticTextFPS;
};


Observa como esta clase ya se asemeja a una formulario de M$Windows, con sus controles, eventos, etc. Además tiene dos objetos especialmente interesantes, el editor y el simulador, que también heredan de IApplication. De hecho, creo que tenia que haberlos declarado como IApplication, mea culpa. Veamos ahora la implementación. Sólo voy a poner los metodos importantes, mInit y mLoop, además de dos eventos importantes, los que lanzan al editor y al simulador respectivamente.

bool
MainApplication::mInit()
{
f_eventReceiver = new EventReceiver(this);
f_device->setEventReceiver(f_eventReceiver);

//Inicializacion de controles
f_menu = f_env->addMenu(0, f_genIDs->getNewID());
f_buttonQuit = f_env->addButton(rect<s32>(10,210,100,240), 0, f_genIDs->getNewID(), L"Quit");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonQuit->getID(), MainApplication::mButtonQuit_Click);
f_buttonNewWindow = f_env->addButton(rect<s32>(10,250,100,290), 0, f_genIDs->getNewID(), L"New Window");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonNewWindow->getID(), MainApplication::mButtonNewWindow_Click);
f_buttonFileOpen = f_env->addButton(rect<s32>(10,300,100,340), 0, f_genIDs->getNewID(), L"File Open");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonFileOpen->getID(), MainApplication::mButtonFileOpen_Click);

f_buttonAddLogo = f_env->addButton(rect<s32>(10,350,100,370), 0, f_genIDs->getNewID(), L"Add Logo");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonAddLogo->getID(), MainApplication::mButtonAddLogo_Click);
f_buttonRemoveLogo = f_env->addButton(rect<s32>(10,380,100,400), 0, f_genIDs->getNewID(), L"Remove Logo");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonRemoveLogo->getID(), MainApplication::mButtonRemoveLogo_Click);

f_buttonLaunchEditor = f_env->addButton(rect<s32>(10,410,100,430), 0, f_genIDs->getNewID(), L"Editor");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonLaunchEditor->getID(), MainApplication::mButtonLaunchEditor_Click);
f_buttonLaunchSimulator = f_env->addButton(rect<s32>(10,440,100,460), 0, f_genIDs->getNewID(), L"Simulator");
f_eventReceiver->mAddGuiEvent(EGET_BUTTON_CLICKED, f_buttonLaunchSimulator->getID(), MainApplication::mButtonLaunchSimulator_Click);

f_staticTextTransparentControl = f_env->addStaticText(L"Transparent Control:", rect<s32>(150,20,350,40), true,true,0,f_genIDs->getNewID(),false);

f_scrollbar = f_env->addScrollBar(true,rect<s32>(150, 45, 350, 60), 0, f_genIDs->getNewID());
f_scrollbar->setMax(255);
f_scrollbar->setPos(127);
f_eventReceiver->mAddGuiEvent(EGET_SCROLL_BAR_CHANGED, f_scrollbar->getID(), MainApplication::mScrollBar_Changed);
f_staticTextLogger = f_env->addStaticText(L"Logging ListBox:", rect<s32>(50,80,250,100), true,true,0,f_genIDs->getNewID());

f_listBoxLogger = f_env->addListBox(rect<s32>(50, 110, 250, 180),0,f_genIDs->getNewID());

f_skin = f_env->getSkin();
f_font = f_env->getFont("media/fonthaettenschweiler.bmp");
if (f_font) f_skin->setFont(f_font);
f_ImageLogo= f_env->addImage(
f_driver->getTexture("media/irrlichtlogo.png"),
position2d<int>(f_driver->getScreenSize().Width - 128,f_driver->getScreenSize().Height- 105));

//Anyadimos el texto de los fps
f_staticTextFPS = f_env->addStaticText(L"", core::rect<s32>(10,10,100,26), true, false, 0,f_genIDs->getNewID());

// IGUIWindow* subventana = f_env->addWindow(rect<s32>(0,0,800,600), true,L"titulo de la subventana",0,2000);
// IGUIButton *subboton = f_env->addButton(rect<s32>(50,50,50+100,50+32), subventana, 2001, L"Quit");
return true;
}

bool
MainApplication::mRemove()
{
f_menu->remove();
f_buttonLaunchSimulator->remove();
f_buttonLaunchEditor->remove();
f_buttonQuit->remove();
f_buttonNewWindow->remove();
f_buttonFileOpen->remove();
f_buttonAddLogo->remove();
f_buttonRemoveLogo->remove();
f_staticTextTransparentControl->remove();
f_scrollbar->remove();
f_staticTextLogger->remove();
f_listBoxLogger->remove();
if(f_ImageLogo) f_ImageLogo->remove();
f_staticTextFPS->remove();
return true;
}

void
MainApplication::mButtonLaunchEditor_Click(void *o, const SEvent & event)
{
MainApplication* me = (MainApplication*)o;
me->mRemove();
me->f_editor = new EditorApplication(me->f_device);
me->f_editor->mRun();
me->mInit();
}

void
MainApplication::mButtonLaunchSimulator_Click(void *o, const SEvent & event)
{
MainApplication* me = (MainApplication*)o;
me->mRemove();
me->f_simulador = new SimulatorApplication(me->f_device);
me->f_simulador->mRun();
me->mInit();
}

El método mInit es el encargado de inicializar los controles y configurar a f_eventReceiver. Después de él, viene mRemove, encargado de retirar todos los controles y de asear el IrrlichtDevice. Esto lo hacemos así porque no podemos eliminar ni reasignar el objeto IGUIEnvironment del IrrlichtDevice, con lo que cada objeto IAplicación que recibe el IrrlichtDevice, pone sus controles y eventos y al terminar los quita del mismo.

Fijaos en los eventos que lanzan al editor y al simulador. Antes de instanciar, por ejemplo, el editor, se invoca a mRemove de MainApplication, para que al instanciar el editor, éste reciba el IrrlichrtDevice limpio y aseado. Tambien puede ocurrir que no sea necesario quitar todos los controles o incluso mantener algunos.

El EditorApplication y SimulatorApplication son parecidos a MainApplication, salvo que cada uno tendrá sus controles y funcionalidad.

Bueno, con esto hemos terminado el bloque de Estructura de menus Irrlicht. Conforme he ido escribiendo estas dos últimas entradas me he ido dando cuenta de algunos defetos de diseño, como el tema de IApplication como interfaz pura y otras interfaces que se podrían implementar. Será cuestion de corregislas.

Se me olvidaba deciros que en la cajita que tenemos de Box.Net hay un ejemplo que pusimos al principio de tener el blog. si lo pruebas podrás ver el menu principal y te permitira pasar al simulador o al editor. en el simulador presiona la tecla 'c' para liberar el puntero del raton y poder pulsar en el boton para volver al menú principal.

Un saludo.

Sigue leyendo >

La clase interfaz de ventana IApplication

Seguimos hoy con el bloque que trata de la estructura de ventanas con Irrlicht. Ahora nos toca ver la interfaz que nos permitirá implementar las distintas secciones de la aplicación.


Veamos la declaración de la clase.
#include <irrlicht/irrlicht.h>
#include "EventReceiver.h"
#include "GeneraID.h"
#include "FPSManager.h"

using namespace irr;
using namespace core;
using namespace scene;
using namespace video;
using namespace io;
using namespace gui;

class IApplication
{
public:
IApplication(IrrlichtDevice *device);
virtual ~IApplication();
virtual bool mRun();
virtual bool mInit() = 0;
virtual bool mRemove() = 0;
virtual bool mLoop();
bool getRunLoop();
void setRunLoop(bool value);

protected:
virtual bool mPreDraw();

protected:
IrrlichtDevice *f_device;
EventReceiver *f_eventReceiver;
GeneraID *f_genIDs;
IVideoDriver *f_driver;
IGUIEnvironment *f_env;
ISceneManager *f_smgr;

bool f_runLoop;
IFPSManager *f_FPSManager;
};


He de decir que me equivoqué con el nombre, no tenía que haberse llamado IApplication, puesto que no es una interfaz pura, tiene propiedades y metodos implementados (lo siento por los puristas del diseño ;)

En su parte privada vemos que tiene punteros a objetos que ya comentamos, como el IrrlichtDevice, EventReceiver, IFPSManager; otros de Irrlicht como IVideoDriver, IGUIEnvironment e ISceneManager y uno de cosecha propia, el GeneraID.
Los objetos de irrlicht vereis que son extraidos del IrrlichtDevice para usarlos con mayor comodidad.

En cuanto a GeneraID, vereis que es necesario para, como su nombre indica, generar identificadores únicos para los controles del GUI. Recordais el ejemplo típico del EventReceiver de los tutoriales? También hay que destacar que para que los id's sean verdaderamente únicos, solo puede haber una instancia de la clase. Esto se consigue con el patron de diseño singleton. He aqui su declaración:
#include <iostream>
#include <irrlicht/irrlicht.h>

using namespace irr;

class GeneraID
{
public:
static GeneraID* getInstance();
s32 getNewID();
s32 getLastID();
virtual ~GeneraID();

protected:
GeneraID(s32 initial = 0);
s32 f_id;
static GeneraID* s_instancia;
};

Y su implementación.
#include"GeneraID.h"
GeneraID* GeneraID::s_instancia = 0;

using namespace irr;

GeneraID*
GeneraID::getInstance()
{
if(!s_instancia) s_instancia = new GeneraID();
return s_instancia;
}

GeneraID::GeneraID(s32 initial)
{
f_id = initial >= 0 ? initial : 0;
}

GeneraID::~GeneraID()
{
}

s32
GeneraID::getNewID()
{
return ++f_id;
}

s32
GeneraID::getLastID()
{
return f_id;
}


volviendo a IApplication, en la parte pública, tenemos métodos implementados como mRun, mLoop, y los métodos get y set del atributo f_runLoop. mRun y mLoop nos permiten ejecutar el bucle de ejecución de la aplicación y mediante f_runLoop, podemos parar la ejecución del mismo.

Veamos ahora la implemetación de IApplication.
#include "IApplication.h"

IApplication::IApplication(IrrlichtDevice *device)
{
f_device = device;
f_driver = f_device->getVideoDriver();
f_env = f_device->getGUIEnvironment();
f_smgr = f_device->getSceneManager();
f_genIDs = GeneraID::getInstance();
f_eventReceiver = NULL;
f_runLoop = true;
f_FPSManager = new FPSManager(f_device);
}

IApplication::~IApplication()
{
f_device = NULL;
f_driver = NULL;
f_env = NULL;
f_smgr = NULL;
f_genIDs = NULL;
f_runLoop = false;
if(f_FPSManager != NULL)
delete f_FPSManager;
f_FPSManager = NULL;
}

bool
IApplication::mRun()
{
if(!mInit()) return false;
if(!mLoop()) return false;
if(!mRemove()) return false;
return true;
}

bool
IApplication::mLoop()
{
while(f_runLoop && f_device->run() && f_driver)
{
f_FPSManager->mUpdateFPS();
if (f_device->isWindowActive())
{
f_FPSManager->mDrawFPS();
mPreDraw();

f_driver->beginScene(true, true, SColor(0,122,65,171));
f_smgr->drawAll();
f_env->drawAll();
f_driver->endScene();
}
else
f_device->yield();
}

//f_device->drop();
return true;
}

bool
IApplication::getRunLoop()
{
return f_runLoop;
}

void
IApplication::setRunLoop(bool value)
{
f_runLoop = value;
}

bool
IApplication::mPreDraw()
{
return true;
}


Podemos observar que el contructor no tiene nada especial, tan solo que recibe el IrrlichtDevice y recupera de él, el videoDriver, GUIEnvironment y SceneManager.

El metodo mLoop es el interesante. Pese a que es un bucle típico de los tutoriales de Irrlicht, le hemos agregado un par de cosillas, tales como la gestion de los FPS y en método mPreDraw(), que lo implementarán las clases sucesoras si hiciese falta. mPreDraw() surgió como necesidad en el simulador, que necesita procesar instucciones ODE antes de dibujar en ventana.

En la próxima entrega veremos ejemplos de uso de IApplication con el menú principal, el editor y el simulador.

Sigue leyendo >