Creando un sistema de camara en primera persona simple Estilo de camara Quake 3 Arena


Introduccion


Si, quieres usar alguna clase de camara en primera persona al estilo de Quake 3, ademas de un estilo de camara libre con un pitch limitado entre -90 grados y 90 grados, esta es tu pagina!

Para hacerlo relativamente simple y potente, vamos a usar multiples SceneNodes. No te preocupes si nunca los has usado, es un concepto simple, al menos, para lo que lo vamos a usar.

Comprension basica del concepto de SceneNode


Un SceneNode es solo una clase especial de "contenedor' espacial. Puede contener solo otros SceneNode al igual que Entity, Camera y otros SceneNode heredados y las clases MovableObject ( y sus propias herencias ).

Por que deberia preocuparme por ellos? Porque cada SceneNode tiene su propio espacio de transformaciones. Las transformaciones son scale (escalar), rotation (rotacion de la orientacion) y translation (traslacion de la posicion). Pero que es todavia mas interesante es que tambien heredan de su espacio padre de transformaciones.

Un pequenyo ejemplo sera mejor que mil palabras: tenemos un SceneNode B que hereda del SceneNode A. Entonces, aplicamos una rotacion de 90 grados alrededor del eje Y del SceneNode A. Ahora, si leemos el angulo de orientacion del eje Y del SceneNode B (tambien veremos como hacer eso en este caso particular), podemos ver que es de 90 grados!.

Para quienes no lo hayan comprendido o realizado, si El SceneNode B podria no heredar del SceneNode A, cuando debieramos leer el angulo de orientacion del eje Y del SceneNode B, deberia devolver 0 grados (la orientacion por defecto), olvidate del hecho del angulo de 90 grados de la orientacion del eje Y del SceneNode A.

Nota que esto es tambien cierto para la herencia del MovableObject (Entity, Camera, etc...)

Esto es que la propiedad de herencia del espacio de transformacion de SceneNode-SceneNode y SceneNode-MovableObject que vamos a usar para construir nuestra camara en primera persona.

Asi, que vamos :-)

Los detalles sobre como conseguir el efecto deseado


Aqui esta el fondo de nuestra jerarquia SceneNode:

   cameraNode (Ogre::SceneNode *)
              ||
              \/
  cameraYawNode (Ogre::SceneNode *)
              ||
              \/
  cameraPitchNode (Ogre::SceneNode *)
              ||
              \/
  cameraRollNode (Ogre::SceneNode *)
              ||
              \/
       camera (Ogre::Camera *)


cameraNode sera el SceneNode mas alto de la jerarquia. Esto va a manejar la posicion de la camara (-+camera+- que heredara su espacio de traslacion de transformacion desde el cameraRollNode del que heredara desde su cameraPitchNode, etc ...).

Como probablemente habras adivinado, el cameraYawNode sera manejado... camera "yaw" su orientacion, el cameraPitchNode manejara el pitch de la camera "la orientacion" y, finalmente, el cameraRollNode manejara la el camera Roll "orientacion".

Los ángulos de Euler y el efecto de cierre de gimbal


Cada rotacion en el espacio de trasformacion de los angulos del eje (orientacion) seran independientes de los otros. El yaw, el pitch y el roll seran hermeticamente separados. Esto evitara el efecto de cierre del gimbal, heredado de la manipulacion de angulos de Euler (yaw, pitch y roll).

El efecto aparece tan pronto como cambias el valor de uno de estos tres angulos, tambien puedes cambiar los otros dos. Eso resulta en el desorden completo de la orientacion y una rotacion loca, que es el efecto de cierre gimbal asi llamado.

¿Qué hay sobre yaw, pitch y roll?


Pero debes estar maravillado: "Que es el yaw, el pitch y el roll?".

Bien, simplemente ponlo, el yaw es el angulo de rotacion alrededor del eje Y, pitch es alrededor del eje X y el roll es alrededor del eje Z.

Sumérgete en el código


Primero, declaramos algunas nuevas variables en nuestra clase FrameListener:

Ogre::SceneNode *cameraNode;
Ogre::SceneNode *cameraYawNode;
Ogre::SceneNode *cameraPitchNode;
Ogre::SceneNode *cameraRollNode;

Entonces, saltamos dentro del constructor FrameListener e inicializamos estas nuevas variables del modo correcto con respecto al fondo que dibujamos antes:

// Crea el nodo superior de camara (el cual solo manejara la posicion).
this->cameraNode = this->sceneManager->getRootSceneNode()->createChildSceneNode();
this->cameraNode->setPosition(0, 0, 500);
 
// Crea el nodo yaw de la camara como el hijo del nodo superior de la camara.
this->cameraYawNode = this->cameraNode->createChildSceneNode();
 
// Crea el nodo pitch de la camara como un hijo del nodo pitch de la camara.
this->cameraPitchNode = this->cameraYawNode->createChildSceneNode();
 
// Crea el nodo roll de la camara como hijo del nodo pitch de la camara
// y acopla la camara a el.
this->cameraRollNode = this->cameraPitchNode->createChildSceneNode();
this->cameraRollNode->attachObject(this->camera);



La primera piedra ha sido arrojada

Date cuenta de que debes asegurarte de que tu camara y el nodo yaw, y el roll de la camara este posicionado en (0,0,0), que la posicion por defecto. Asegurate de no llamar a this->camera->setPosition(someX, someY, someZ) o this->camera->translate(someOtherX, someOtherY, someOtherZ) en cualquier parte en tu codigo del programa (o cancelar su efecto llamando a this->camera->setPosition(0.0f, 0.0f, 0.0f)). Sino quieres efectos no deseados que la camara rote alrededor de un punto (Y lo se por experiencia).

Ahora, borra todos los FrameListeners el contenido del metodo MoveCamera() (no eliminar el metodo en si mismo, solo su contenido) y reemplazalo por lo siguiente:


Ogre::Real pitchAngle;
Ogre::Real pitchAngleSign;
 
// Yaws la camara de acuerdo al movimiento relativo del raton.
this->cameraYawNode->yaw(this->mRotX);
 
// Pitches la camara de acuerdo al movimiento relativo del raton.
this->cameraPitchNode->pitch(this->mRotY);
 
// traslada la camara de acuerdo al vector traslacion que es controlado por las flechas del teclado.
// NOTA: Multiplicamos el mTranslateVector por el quaternion de orientacion del cameraPitchNode
// y el quaternion de orientacion de la cameraYawNode para trasladar la camara de acuerdo a la orientacion de la camara
// alrededor del eje Y y el eje X.
 
this->cameraNode->translate(this->cameraYawNode->getOrientation() * this->cameraPitchNode->getOrientation() * this->mTranslateVector, Ogre::SceneNode::TS_LOCAL);
 
// Angulo de rotacion alrededor del eje X.
pitchAngle = (2 * Ogre::Degree(Ogre::Math::ACos(this->cameraPitchNode->getOrientation().w)).valueDegrees());
 
// Solo para determinar el signo del angulo que escogimos antes, el
// valor en si mismo no nos interesa.
pitchAngleSign = this->cameraPitchNode->getOrientation().x;
 
// Limite del pitch entre -90 grados y +90 grados, al estilo de Quake.
if (pitchAngle > 90.0f)
{
    if (pitchAngleSign > 0)
      // Establece la orientacion a 90 grados en el eje X.
      this->cameraPitchNode->setOrientation(Ogre::Quaternion(Ogre::Math::Sqrt(0.5f),Ogre::Math::Sqrt(0.5f), 0, 0));
    else if (pitchAngleSign < 0)
      // Establece la orientacion a -90 grados en el eje X.
      this->cameraPitchNode->setOrientation(Ogre::Quaternion(Ogre::Math::Sqrt(0.5f),Ogre::Math::Sqrt(0.5f), 0, 0));
}
Entonces, reemplazamos algunos controles por defecto y anyadimos algunos otros en el FrameListener en el metodo processUnbufferedKeyInput() (Asumo que esta basado en el ExempleFrameListener, sino deberias ser capaz de aplicarlo a tu caso particular ;-]):

// Mueve la camara hacia arriba en el eje Y del mundo.
if(inputManager->isKeyDown(Ogre::KC_PGUP))
    // this->translateVector.y = this->moveScale;
    this->cameraNode->setPosition(this->cameraNode->getPosition() + Ogre::Vector3(0, 5, 0));
 
// Mueve la camara hacia abajo en el eje Y del mundo.
if(inputManager->isKeyDown(Ogre::KC_PGDOWN))
    // this->translateVector.y = -(this->moveScale);
    this->cameraNode->setPosition(this->cameraNode->getPosition() - Ogre::Vector3(0, 5, 0));
 
// Mueve la camara adelante.
if(inputManager->isKeyDown(Ogre::KC_UP))
    this->translateVector.z = -(this->moveScale);
 
// Mueve la camara hacia atras.
if(inputManager->isKeyDown(Ogre::KC_DOWN))
    this->translateVector.z = this->moveScale;
 
// Mueve la camara a izquierdas.
if(inputManager->isKeyDown(Ogre::KC_LEFT))
    this->translateVector.x = -(this->moveScale);
 
// Mueve la camara a derechas.
if(inputManager->isKeyDown(Ogre::KC_RIGHT))
    this->translateVector.x = this->moveScale;
 
// Rota la camara a la izquierda.
if(inputManager->isKeyDown(Ogre::KC_Q))
    this->cameraYawNode->yaw(this->rotateScale);
 
// Rota la camara a la derecha.
if(inputManager->isKeyDown(Ogre::KC_D))
    this->cameraYawNode->yaw(-(this->rotateScale));
 
// Desnuda toda la rotacion yaw en la camara.
if(inputManager->isKeyDown(Ogre::KC_A))
    this->cameraYawNode->setOrientation(Ogre::Quaternion::IDENTITY);
 
// Rota la camara hacia arriba.
if(inputManager->isKeyDown(Ogre::KC_Z))
    this->cameraPitchNode->pitch(this->rotateScale);
 
// Rota la camara hacia abajo.
if(inputManager->isKeyDown(Ogre::KC_S))
   this->cameraPitchNode->pitch(-(this->rotateScale));
 
// Desnuda toda la rotacion del pitch en la camara.
if(inputManager->isKeyDown(Ogre::KC_E))
   this->cameraPitchNode->setOrientation(Ogre::Quaternion::IDENTITY);
 
// mueve la camara hacia la izquierda.
if(inputManager->isKeyDown(Ogre::KC_L))
   this->cameraRollNode->roll(-(this->rotateScale));
 
// mueve la camara hacia la derecha.
if(inputManager->isKeyDown(Ogre::KC_M))
   this->cameraRollNode->roll(this->rotateScale);
 
// Desnuda todo lo aplicado a la camara.
if(inputManager->isKeyDown(Ogre::KC_P))
   this->cameraRollNode->setOrientation(Ogre::Quaternion::IDENTITY);
 
// Desnuda todas las rotaciones a la camara.
if(inputManager->isKeyDown(Ogre::KC_O))
{
   this->cameraYawNode->setOrientation(Ogre::Quaternion::IDENTITY);
   this->cameraPitchNode->setOrientation(Ogre::Quaternion::IDENTITY);
   this->cameraRollNode->setOrientation(Ogre::Quaternion::IDENTITY);
}


Si quieres ver cual es la orientacion actual alrededor de cada eje, aqui esta un modo de hacerlo: copia el siguiente codigo en el metodo updateStats() del FrameListener (en el bloque de codigo try{}).

this->renderWindow->setDebugText("Camera orientation: ("
+ ((this->cameraYawNode->getOrientation().y >= 0) ? std::string("+") :
std::string("-")) + "" + Ogre::StringConverter::toString(Ogre::Math::Floor(2 *
Ogre::Degree(Ogre::Math::ACos(this->cameraYawNode->getOrientation().w)).valueDegrees())) + ", " +
((this->cameraPitchNode->getOrientation().x >= 0) ? std::string("+") : std::string("-")) + "" +
Ogre::StringConverter::toString(Ogre::Math::Floor(2 *
Ogre::Degree(Ogre::Math::ACos(this->cameraPitchNode->getOrientation().w)).valueDegrees())) + ", " +
((this->camera->getOrientation().z >= 0) ? std::string("+") : std::string("-")) + "" +
Ogre::StringConverter::toString(Ogre::Math::Floor(2 *
Ogre::Degree(Ogre::Math::ACos(this->camera->getOrientation().w)).valueDegrees())) + ")");


Es es todo! Ahora, deberias tener una camara en primera persona :-) solo echa un vistazo al codigo, asegurate de que no has comentado o se te haya olvidado ninguna parte de este tutorial, compilalo y disfruta!

Algunas anotaciones


Alguna gente me pregunta: "Por que deberia manejar un nodo roll de camara? Podria aplicar la rotacion roll directamente a la camara!. Si, estas en lo cierto, y no tendrias que cambiar nada, excepto que deberias conseguir un SceneNode Intermediario. Para ser honestos, lo he hecho en mi propio codigo, pero guardemos esto como un secreto, de otra forma podria ser quemado por los puristas

El orden de los SceneNodes es importante para el sistema de camara FPS, asi cuando conectas el primer cameraPitchNode y entonces el cameraYawNode, conseguiras una transformacion diferente completa. Porque las multiplicaciones de Matrices no son conmutativas. Recuerda esto e intentalo por ti mismo.

Y para aplicarlos por separado - esto es necesario para el efecto de cierre del gimbal. Para mas informacion sobre los quaterniones, comprueba el articulo Quaterniones y rotacion de Primer Nivel.

Finalmente, Podria decir que la misma regla que he explicado en el parrafo anterior se aplica a la traslacion del Nodo de camara (para que la camara se mueva dentro del mundo de acuerdo con las teclas de flechas del teclado) sobre el orden de los quaterniones de orientacion (tu sabes, los metodos getOrientation()). Tambien, debido a muy poco conocimiento sobre los quaterniones y los vectores, deberias respetar el orden de la multiplicacion entre el translateVector y los quaterniones de orientacion.

Las ultimas palabras, te preguntaras porque no he incluido el quaternion cameraRollNode en la multiplicacion dada al metodo translate() del cameraNode: esto es asi porque No quiero que la rotacion roll de my camara influencie su desplazamiento, esto es asi de simple :-)

Nota: No comprendo bastante el trabajo actual, pero encontre que modificando un nodo de camara sin actualizar todos los subyacentes resultara en una actualizacion de camara retrasada. La solucion es establecer la llamada a needUpdate() en todos los nodos (recursivamente) acoplandolo al nodo que he actualizado. Ej: Si llamas a cameraYawNode->yaw(grados) deberias llamar a cameraPitchNode->needUpdate() y cameraRollNode->needUpdate() tambien. -nyxkn