Luces, sombras y entorno

De IberOgre

Gracias a los artículos Creación básica de escenas y Manipulación de nodos ya sabemos construir escenas sencillas en las que disponer personajes, objetos, etc para después manipularlos dentro del bucle de juego.

En cualquier juego 3D que se precie la iluminación juega un papel de suma importancia, en este texto aprenderemos a crear fuentes de luz de distintos tipos para conseguir los efectos deseados. Mostraremos varias clases de sombras y daremos pistas para elegir la que mejor se adapte a tus necesidades. Así mismo, aprenderemos a limitar el renderizado con efectos como la niebla en el caso de que nuestro videojuego trate de mostrar entornos demasiado grandes. Por último, comentaremos diversas técnicas para renderizar fondos de forma sencilla, por ejemplo, un cielo azul.


Contenido



Requisitos mínimos

Las técnicas que se desglosarán en los apartados que siguen suponen un cierto grado de manejo del engine de renderizado Ogre3D. Por tanto, es recomendable que hayas leído y trabajado los ejemplos de artículos anteriores. Podrías acudir a:

  • Creación de escenas: vamos a explicar cómo añadir efectos y elementos nuevos a tus escenas. Es lógico que llegues a este artículo sabiendo como dar los primeros pasos al respecto. Es recomendable que leas Creación básica de escenas.
  • Gestión de recursos: no haremos un uso intensivo de los recursos en este artículo pero serán útiles en alguna ocasión. El sistema de recursos de Ogre3D puede parecer confuso al principio pero es sencillo de entender y muy potente. Podrías acudir al artículo Gestión de recursos.
  • OIS: en el ejemplo final vamos a trabajar con la entrada del usuario para desplazarnos por la escena. En IberOgre utilizamos OIS como biblioteca para capturar y gestionar eventos producidos por el teclado y el ratón. Te remitimos al artículo publicado al respecto en la wiki, Manejo básico de OIS.


Sombras

En el RTS Warzone 2010 no todos los elementos proyectan sombras

El hecho de que los elementos del juego proyecten sombras sobre el entorno o incluso ellos mismos le otorga un punto importante de realismo al proyecto. Hace no mucho tiempo a lo más que se podía aspirar era a una elipse oscura a los pies del personaje, ¡y eso ya nos impresionaba a muchos! Hoy en día, los motores de renderizado aceptan varios tipos de sombras solapándose en función de las características del objeto y la iluminación del entorno. La calidad de las mismas varía en función del juego. Algunas son más estáticas o menos detalladas mientras que otras se modulan en tiempo real de una forma asombrosa.

A estas alturas estamos acostumbrados a que Ogre disponga de subsistemas altamente configurables y el de las sombras no podía ser menos. Como desarrolladores podemos elegir la calidad de las sombras que se renderizarán en cada momento. Es más, podemos elegir si la escena constará de sombras o estas no aparecerán en absoluto. Incluso se nos permite elegir qué entidades concretas proyectarán sombras y cuáles no. En esta sección nos dedicaremos a explicar cómo configurar las sombras en Ogre.

Tipos de sombras

Ogre soporta varios tipos de sombras distintas, cada una tiene una calidad y un consumo de recursos determinados. Lógicamente, una mayor calidad implicará un mayor tiempo de renderizado. Eres el único responsable de elegir el tipo de sombreado que poseerá tu juego. Para tomar la decisión correcta debes tener en cuenta aspectos como la plataforma destino (requisitos mínimos) y la carga de entidades que tendrán tus escenas. En Ogre el tipo de sombra se representa mediante el enumerado Ogre::ShadowTechnique. Si acudes a su sección en la documentación oficial verás que existen múltiples combinaciones. Aquí nos limitaremos a comentar las más utilizadas.

Listaremos 4 técnicas de sombreado que surgen por las combinaciones de 2 factores: "texture"/"stencil" y "modulative"/"additive". La diferencia entre las variantes del primer factor escapan al alcance de este artículo. No obstante, las diferencias entre las variantes del segundo factor son mucho más sencillas de comprender:

  • "Modulative": el renderizado de las sombras es relativamente simple ya que simplemente se calcula si una luz afecta al objeto y, en tal caso se crea una sombra. Se trata de una sombra monocroma ya que la interacción de varias luces sobre una misma proyección no se tiene en cuenta. La técnica no es muy costosa pero no produce resultados espectaculares.
  • "Additive": con esta técnica se renderiza la escena desde la perspectiva de la cámara. Posteriormente se calcula la contribución de cada luz presente al objeto a través de pasadas adicionales. Como es lógico, el resultado final es tremendamente preciso pero a costa de un consumo importante de tiempo.

Con la combinación de los 2 factores obtenemos los siguientes 4 tipos de sombreado.

  • SHADOWTYPE_TEXTURE_MODULATIVE
  • SHADOWTYPE_TEXTURE_ADDITIVE
  • SHADOWTYPE_STENCIL_MODULATIVE
  • SHADOWTYPE_STENCIL_ADDITIVE

Para comprender las diferencias entre las técnicas "modulative" y "additive" puedes mirar el siguiente par de imágenes. Ambas muestran exactamente la misma escena en cuanto a elementos y sus propiedades. Tan sólo se diferencian en la técnica de sombreado del gestor de escena, incluso las propiedades de las luces son las mismas.

Ejemplo de escena con un sombreado de tipo SHADOWTYPE_STENCIL_MODULATIVE
Ejemplo de escena con un sombreado de tipo SHADOWTYPE_STENCIL_ADDITIVE

Trabajando con sombras

En esta sección daremos detalles sobre los métodos que ofrece Ogre para trabajar con las sombras. No sólo podemos seleccionar la técnica de sombreado general de la escena. Además, seremos capaces de elegir incluso qué entidades proyectan sombras y cuales no. Trabajar con sombras es sencillo y saber manejarlas nos ayudará a conseguir mejores resultados en nuestras aplicaciones 3D.

Es posible seleccionar el tipo de sombreado con el que contará la escena. Al ser una propiedad de la escena es lógico pensar que dicho método pertenecerá a nuestro SceneManager. Concretamente, emplearemos el método SceneManager::setShadowTechnique.

void Ogre::SceneManager::setShadowTechnique(ShadowTechnique technique)
  • ShadowTechnique technique: técnica de sombreado deseada, uno de los valores del enumerado Ogre::ShadowTechnique que listamos en el apartado anterior.

Para trabajar con el sombreado de los elementos concretos de la escena debemos descender al nivel de la clase Entity. Empezaremos consultando si una entidad proyecta sombras mediante el método Entity::getCastShadows:

bool Ogre::Entity::getCastShadows(void) const;

Lo usual es decidir si una entidad proyecta sombras en el momento de su creación, antes de ser unida a un SceneNode. Como ya habrás imaginado a estas alturas, el método para esa tarea se llama Entity::setCastShadows. Por defecto, las entidades proyectan sombras.

void Ogre::Entity::setCastShadows(bool enabled);
  • bool enabled: true para activar las sombras, false para desactivarlas.

Básicamente gestionamos el sombreado de la escena en dos sencillos pasos. En primer lugar elegimos la técnica de sombreado general y cada vez que creamos una entidad le indicamos si queremos que proyecte sombras o no. El siguiente fragmento de código ilustra este proceso para aclararlo de forma definitiva.

// Por simplicidad partimos de:
// Ogre::SceneManager* gestorEscena;
 
// AL CONFIGURAR EL SCENEMANAGER
 
// Elegimos la técnica de sombreado general
gestorEscena->setShadowTechnique(SHADOWTYPE_STENCIL_ADDITIVE);
 
 
// AL CREAR LA ESCENA
 
// Creamos una entidad que proyecte sombras (personaje)
// Por defecto proyecta sombras, no hacemos nada
Ogre::Entity entidadPersonaje = gestorEscena->createEntity("entidadPersonaje", "personaje.mesh");
Ogre::SceneNode* nodoPersonaje = gestorEscena->getRootSceneNode()->createChildNode("nodoPersonaje");
nodoPersonaje->attachObject(entidadPersonaje);
 
// Creamos una entidad que no proyecte sombras (roca)
Ogre::Entity entidadRoca = gestorEscena->createEntity("entidadRoca", "roca.mesh");
entidadRoca->setCastShadows(false);
Ogre::SceneNode* nodoRoca = gestorEscena->getRootSceneNode()->createChildNode("nodoRoca");
nodoRoca->attachObject(entidadRoca);


Iluminación

Scorched 3D es un juego de estrategia en el que la iluminación exterior y los reflejos le dan un toque especial

La iluminación juega un papel fundamental en el mundo de los videojuegos. Tal y como sucede en otros tipo de formas de expresión visuales (cine, pintura, novelas gráficas...), la iluminación nos ayuda a crear la ambientación deseada. Es posible que recordemos muchos juegos por los efectos de luces que utilizaban. No es trivial colocar puntos de luz de la manera necesaria para producir el resultado que buscamos.

En esta sección no vamos a proporcionar un curso sobre iluminación (eso es trabajo de los diseñadores del juego). En cambio, nos limitaremos a exponer los tipos de luces que nos proporciona Ogre y detallaremos el uso de cada una de ellas. El objetivo es que aprendas para qué se utiliza cada clase de luz y conozcas los métodos para su manipulación. Se listarán ejemplos a tal efecto.

Tipos de luces

En Ogre3D existen 3 tipos de luces que proporcionan efectos diferentes, el uso de un tipo o de otro dependerá únicamente de nuestras necesidades. Los 3 tipos de luces se gestionan de manera uniforme mediante la clase Ogre::Light. Para indicar el tipo de una luz utilizaremos el enumerado Ogre::Light::LightTypes. En definitiva, estos son las luces disponibles (entraremos en detalle en las subsecciones que siguen):

  • Punto de luz: representado mediante el valor Ogre::Light::LT_POINT. Como su propio nombre indica, es un punto que emite luz en todas direcciones.
  • Foco: representado mediante el valor Ogre::Light::LT_SPOTLIGHT. Su funcionamiento es muy intuitivo, tenemos la luz situada en un punto y cuenta con una dirección. Podemos definir el ángulo de amplitud y su potencia pero eso lo veremos en la sección al respecto.
  • Luz direccional: representada mediante el valor Ogre::Light::LT_DIRECTIONAL. Se utiliza para modelar una fuente de luz muy lejana que afecta a toda la escena con una dirección.

Independientemente del tipo de luz con la que deseemos trabajar, existen propiedades comunes. No obstante, según el tipo de luz algunos conceptos pueden dejar de tener sentido como la posición o la dirección. En las secciones dedicadas a cada tipo de luz, veremos cuáles son las propiedades que tienen o carecen de sentido. Como de costumbre, te remitimos a la sección correspondiente de la documentación oficial para conocer todos los detalles de esta clase ya que aquí nos limitaremos a mostrar un subconjunto de sus posibilidades.

Creación, gestión y destrucción de luces

Las luces son elementos que pertenecen a la escena por lo que su gestión a nivel de creación, consulta y destrucción la haremos a través de nuestro SceneManager (gestor de escena). A continuación listaremos una serie de métodos que nos ayudarán en esta tarea.

Para crear una luz basta con utilizar una de las dos versiones del método SceneManager::createLight como se muestra en las siguientes líneas:

Light* Ogre::SceneManager::createLight();
Light* Ogre::SceneManager::createLight(const String &name);
  • const String &name: nombre que identifica a la luz y nos ayudará a recuperarla si necesitamos una referencia a la misma. Como vemos, es un parámetro opcional, si no se indica, Ogre le asignará un código de forma automática. Los nombres deben ser únicos, si se proporcionarse un nombre existente se lanzará una excepción.

Es posible recuperar una luz anteriormente creada empleando su nombre y el método SceneManager::getLight:

Light* Ogre::SceneManager::getLight(const String& name) const;
  • const String& name: nombre que identifica a la luz que buscamos. Si no existe una luz con el nombre dado, se producirá una excepción.

Si no estamos seguros de la existencia de una luz con un nombre determinado sería arriesgado pedirle a Ogre que nos la devuelva ante el riesgo de excepción. En tal improbable caso es recomendable consultar en primer lugar si existe una luz con el método SceneManager::hasLight:

bool Ogre::SceneManager::hasLight(const String &name) const;
  • const String& name: nombre que identifica a la luz de la que deseamos conocer su existencia.

Es posible que durante el juego queramos destruir una fuente de luz determinada. En tal caso deberemos hacer uso de cualquiera de las dos formas del método SceneManager::destroyLight como se muestra a continuación:

void Ogre::SceneManager::destroyLight(const String& name);
void Ogre::SceneManager::destroyLight(Light* light);
  • const String& name: nombre que identifica a la luz dentro de la escena.
  • Light* light: puntero a la luz que deseamos destruir.

En lugar de destruir una fuente de luz individual puede que desees eliminarlas todas. ¡No te molestes en recorrer todas las luces eliminándolas una a una! Ogre3D hace ese trabajo por tí a través del método SceneManager::destroyAllLights

void Ogre::SceneManager::destroyAllLights(void);

Iluminación general de la escena

Nuestras escenas cuentan con una luz general que carece de posición y dirección. Eso significa que todos los elementos se verán afectados de la misma forma por el color e intensidad de dicha luz. Esta iluminación se conoce como "luz ambiente" o "ambient light". Como hemos visto anteriormente, en Ogre manejamos los colores mediante la clase Ogre::ColourValue. Por defecto, la luz ambiente de nuestra escena será negra, por tanto, no veremos los objetos presentes a menos que dispongamos luces dinámicas como las que trataremos a continuación.

Para consultar la luz ambiente de la escena utilizaremos el método SceneManager::getAmbientLight de la siguiente forma:

const ColourValue& Ogre::SceneManager::getAmbientLight(void) const

En cambio, si deseamos establecer el color de la luz ambiente en nuestra escena emplearemos el método SceneManager::setAmbientLight como se muestra a continuación:

void Ogre::SceneManager::setAmbientLight(const ColourValue& colour);
  • const ColourValue& colour: color para la luz ambiente de la escena. Recordemos que la clase tiene 4 atributos públicos: r, g, b y a. Éstos representan las 4 componentes del espacio de colores RGBA (rojo, verde, azul y alfa).

Siempre es recomendable establecer una luz ambiente mínima. No obstante, para efectos más realistas puede que nos interese una luz direccional, detallada a continuación.

Reflexión especular y difusa

El color de los píxeles con los que se renderizan los objetos de la escena en la pantalla se ve afectado por multitud de parámetros como la luz ambiente, el material de cada objeto o las luces que lo rodean. El color con el que vemos los objetos depende de los que el propio objeto absorbe y los que refleja cuando incide luz sobre él. No me gustaría entrar en detalle en el terreno de la reflexión de la luz pero es inevitable repasar algunos conceptos.

Cuando una luz incide sobre un objeto se puede producir el fenómeno denominado reflexión. Si el objeto es completamente liso a nivel molecular el ángulo del haz reflejado es el mismo que el incidente con respecto a la normal de la superficie. Esto es lo que conocemos como reflexión especular (de espejo). En cambio, si la superficie es rugosa a nivel molecular tendremos muchos haces reflejados con distintos ángulos. Conocemos este suceso como reflexión difusa. Estos conceptos pueden resultar ligeramente extraños, por ello, se adjunta un esquema aclaratorio.

Diferencias entre la reflexión especular y la reflexión difusa

Ogre trabaja de forma intensiva con estos conceptos. Podemos definir qué color queremos que posean nuestras fuentes de luz tanto cuando producen reflexión especular como difusa. Los siguientes métodos se refieren a este concepto.

Podemos consultar el color de la reflexión difusa que produce la luz con el método Light::getDiffuseColour como se indica a continuación:

const ColourValue& Ogre::Light::getDiffuseColour(void) const;

Si deseamos configurar el color de la reflexión difusa que produce nuestra fuente de luz cualquiera de las dos formas del método Light::setDiffuseColour es válido:

void Ogre::Light::setDiffuseColour(const ColourValue& colour);
void Ogre::Light::setDiffuseColour(Real red, Real green, Real blue);
  • const ColourValue& colour: color que adoptará la reflexión difusa.
  • Real red: componente roja de la reflexión difusa.
  • Real green: componente verde de la reflexión difusa.
  • Real blue: componente azul de la reflexión difusa.

Para consultar el color de la reflexión especular que produce una fuente de luz concreta llamaremos al método Light::getSpecularColour.

const ColourValue& Ogre::Light::getSpecularColour(void) const;

En cambio, para configurar el color de la reflexión especular de nuestra fuente de luz existen sendos métodos al respecto, Light::setSpecularColour.

void Ogre::Light::setSpecularColour(const ColourValue &colour);
void Ogre::Light::setSpecularColour(Real red, Real green, Real blue);
  • const ColourValue& colour: color que adoptará la reflexión especular.
  • Real red: componente roja de la reflexión especular.
  • Real green: componente verde de la reflexión especular.
  • Real blue: componente azul de la reflexión especular.

Atenuación

Como es lógico, la potencia de las luces va disminuyendo a medida que nos alejamos de la fuente (en el caso de que el tipo de luz que nos ocupe tenga posición). Llamaremos a esta disminución de potencia "atenuación". Ogre calcula qué cantidad de luz le llega a un objeto mediante una ecuación de segundo grado cuya única variable es la distancia entre la fuente y el afectado. Eso significa que tendremos un factor constante, uno lineal y otro cuadrático. Además, indicaremos una distancia a la que llamaremos rango, a partir de la cual, la luz no afectará a ningún objeto. A continuación, listamos los métodos necesarios para trabajar con la atenuación de una luz.

Si deseamos consultar alguno de los parámetros que conforman la atenuación de una luz utilizaremos el método que nos interese de la siguiente colección (sus nombres son bastante autoexplicativos):

Real Ogre::Light::getAttenuationRange(void) const;
Real Ogre::Light::getAttenuationConstant(void) const;
Real Ogre::Light::getAttenuationLinear(void) const;
Real Ogre::Light::getAttenuationQuadric(void) const;

En cambio, si nuestro objetivo es configurar la atenuación de una luz acudiremos al método Light::setAttenuation tal y como se muestra:

void Ogre::Light::setAttenuation(Real range, Real constant, Real linear, Real quadratic);
  • Real range: distancia a partir de la cual la luz no afecta a ninguna entidad.
  • Real constant: factor constante en la ecuación de la atenuación.
  • Real linear: factor lineal en la ecuación de la atenuación.
  • Real quadratic: factor cuadrático en la ecuación de la atenuación.

Puntos de luz

Los puntos de luz se identifican en Ogre mediante el valor del enumerado Ogre::Light::LT_POINT. Tienen una posición e iluminan su entorno en todas direcciones por igual. Esto quiere decir que el atributo posición tiene sentido mientras que la dirección carece de él. Los puntos de luz nos pueden ser muy útiles para proporcionarle iluminación a las antorchas de un castillo o a las lámparas de un edificio.

Esquema que ilustra las características de los puntos de luz

Las luces están presentes en la escena y tienen una posición determinada sin necesidad de adjuntarse a un SceneNode, ocurre exactamente lo mismo con las cámaras. Sólo los puntos de luz y los focos tienen dirección. Para conocer la posición de una fuente de luz en un momento determinado podemos hacer uso del método Light::getPosition de la forma habitual:

const Vector3& Ogre::Light::getPosition(void) const;

Si nuestra intención es modificar la posición de una fuente de luz acudiremos a una de las dos formas del método Light::setPosition tal y como acostumbramos:

void Ogre::Light::setPosition(const Vector3& vec);
void Ogre::Light::setPosition(Real x, Real y, Real z);
  • const Vector3& vec: vector que representa el punto con la nueva posición de la luz.
  • Real x: componente en el eje x de la nueva posición de la luz.
  • Real y: componente en el eje y de la nueva posición de la luz.
  • Real z: componente en el eje z de la nueva posición de la luz.

Para resumir las propiedades a tener en cuenta a la hora de trabajar con puntos de luz, adjuntamos el siguiente fragmento de código. Crearemos un punto de luz dentro de nuestra escena y lo configuraremos:

// Por simplicidad, asumimos que disponemos de:
// SceneManager* gestorEscena;
 
// Creamos una luz con el nombre "puntoLuz1"
Ogre::Light puntoLuz* = gestorEscena->createLight("puntoLuz1");
 
// Indicamos que su tipo será LT_POINT
puntoLuz->setType(Ogre::Light::LT_POINT);
 
// La colocamos en la escena
puntoLuz->setPosition(20, 5, -10);
 
// Establecemos el color para la reflexión difusa (rojo)
puntoLuz->setDiffuseColour(1.0, 0.0, 0.0);
 
// Establecemos el color para la reflexión especular (azul)
puntoLuz->setSpecularColour(0.0, 0.0, 1.0);

Focos

Los focos se identifican mediante el valor del enumerado Ogre::Light::LT_SPOTLIGHT. Este tipo es muy sencillo de comprender si nos remitimos a su nombre, simplemente es un foco que consta de una posición y una dirección a la que apunta. La zona trasera no estará iluminada mientras que en la delantera apreciaremos un haz de luz. Tendremos un ángulo interior que define una región del espacio sobre la que el foco actúa a plena potencia y un ángulo exterior en el que la luz de la fuente se va desvaneciendo con un factor determinado. Además, existe un factor de transición entre las regiones interior y exterior del haz. Los ángulos interior y exterior sólo gozan de sentido en los focos.

Esquema que ilustra las características de los focos

¿Para qué pueden sernos útiles los focos? Piensa en un juego de terror en el que avanzamos por oscuros pasillos. Sólo disponemos de una linterna con la que detectar a nuestros enemigos. La luz de dicha linterna estará representada por un foco (posición, dirección y ángulos). Los faros de un vehículo también pueden representarse mediante focos.

En primer lugar, enumeraremos los métodos disponibles que nos ofrece la clase Light de Ogre para trabajar con la dirección de una fuente de luz. Recordemos que sólo los focos y las luces direccionales están dotadas de dirección.

Para consultar la dirección actual de un foco de luz (o luz direccional) emplearemos el método Light::getDirection.

const Vector3& Ogre::Light::getDirection(void) const;

Si nuestro objetivo es establecer el valor de la dirección de la fuente de luz acudiremos a una de las formas del método Light::setDirection.

void Ogre::Light::setDirection(const Vector3 &vec);
void Ogre::Light::setDirection(Real x, Real y, Real z);
  • const Vector3 &vec: vector con la dirección que tomará la fuente de luz.
  • Real x: componente x de la dirección que tomará la fuente de luz.
  • Real y: componente y de la dirección que tomará la fuente de luz.
  • Real z: componente z de la dirección que tomará la fuente de luz.

Es el turno de aprender a consultar y configurar los ángulos de acción (interior y exterior) de nuestro haz de luz. En el siguiente listado los estudiaremos con todo lujo de detalles.

Para consultar los ángulos interior y exterior utilizaremos los métodos Light::getSpotlightInnerAngle o Light::getSpotlightOuterAngle según corresponda en cada caso.

const Radian& Ogre::Light::getSpotlightInnerAngle(void) const;
const Radian& Ogre::Light::getSpotlightOuterAngle(void) const;

Si queremos establecer esos valores, podremos hacerlo utilizando los métodos Light::setSpotlightInnerAngle o Light::setSpotlightOuterAngle respectivamente.

void Ogre::Light::setSpotlightInnerAngle(const Radian &val);
void Ogre::Light::setSpotlightOuterAngle(const Radian &val);
  • const Radian &val: ángulo en radianes para el área interior o exterior (según corresponda en cada caso).

En el caso de querer averiguar el valor del factor de transición entre los volúmenes definidos por los ángulos interior y exterior podemos acudir al método Light::getSpotlightFalloff.

Real Ogre::Light::getSpotlightFalloff(void) const;

Para darle un nuevo valor al factor de transición entre ángulos del foco podemos hacer uso del método Light::setSpotlightFalloff.

void Ogre::Light::setSpotlightFalloff(Real val);
  • Real val: valor del nuevo factor de transición entre volúmenes del foco.

A continuación se muestra un ejemplo de uso de una luz de tipo foco. En primer lugar la crearemos para añadirla a la escena y posteriormente estableceremos sus propiedades. Esto aclarará las posibles dudas existentes.

// Por simplicidad, asumimos que disponemos de:
// SceneManager* gestorEscena;
 
// Creamos una luz con el nombre "puntoLuz1"
Ogre::Light focoLuz* = gestorEscena->createLight("focoLuz1");
 
// Indicamos que su tipo será LT_SPOTLIGHT
focoLuz->setType(Ogre::Light::LT_SPOTLIGHT);
 
// La colocamos en la escena
focoLuz->setPosition(20, 5, -10);
 
// Configuramos la dirección (apuntando al origen de coordenadas)
focoLuz->setDirection(0, 0, 0);
 
// Establecemos el color para la reflexión difusa (verde)
focoLuz->setDiffuseColour(0.0, 1.0, 0.0);
 
// Establecemos el color para la reflexión especular (blanca)
focoLuz->setSpecularColour(1.0, 1.0, 1.0);

Luces direccionales

Las luces direccionales se identifican mediante el valor del enumerado Ogre::Light::LT_DIRECTIONAL. Este tipo de luz no tiene posición pero sí dirección. Representa una luz en el infinito que incide sobre todos los elementos de la escena de manera uniforme con una dirección determinada. Esta clase nos será muy útil cuando queramos representar la luz del sol sobre un espacio abierto. El siguiente esquema ilustra lo que representan las luces direccionales.

Esquema que ilustra las características de las luces direccionales

Hemos explicado cómo trabajar con la dirección de las luces y este tipo no aporta propiedades nuevas que no hayamos tratado. Por tanto, nos limitaremos a exponer un ejemplo de su uso:

// Por simplicidad, asumimos que disponemos de:
// SceneManager* gestorEscena;
 
// Creamos una luz con el nombre "puntoLuz1"
Ogre::Light luzDireccional* = gestorEscena->createLight("luzDireccional1");
 
// Indicamos que su tipo será LT_DIRECTIONAL
luzDireccional->setType(Ogre::Light::LT_DIRECTIONAL);
 
// Configuramos la dirección
luzDireccional->setDirection(-1, 1, -1);
 
// Establecemos el color para la reflexión difusa (blanca)
luzDireccional->setDiffuseColour(1.0, 1.0, 1.0);
 
// Establecemos el color para la reflexión especular (blanca)
luzDireccional->setSpecularColour(1.0, 1.0, 1.0);

Adjuntar una luz a un nodo

La clase Light hereda de MovableObject, exactamente igual que la clase Camera. Por tanto, es posible adjuntar una luz a un nodo de la misma forma que podemos trabajar con las cámaras. ¿En qué nos beneficiaría tener una luz adjunta a un nodo si podemos moverla a nuestro antojo con sus propios métodos? Imagina los faros de un vehículo que comentábamos antes, éstos dependen del movimiento del coche continuamente para posicionarse. Si creásemos una jerarquía de nodos de forma que adjuntásemos los faros en subnodos del nodo principal que representase al coche nos ahorraríamos mucho trabajo.

Adjuntar una luz a un nodo es harto sencillo. No obstante, mostramos un pequeño fragmento para ilustrar este proceso:

// Por simplicidad, asumimos que disponemos de:
// Ogre::SceneNode* nodoCoche;
// Ogre::SceneManager* gestorEscena;
 
// Creamos las 2 luces
Ogre::Light* faroA = gestorEscena->createLight("cocheFaroA");
Ogre::Light* faroB = gestorEscena->createLight("cocheFaroB");
 
// Establecemos su tipo (foco)
faroA->setType(Ogre::Light::LT_SPOTLIGHT);
faroB->setType(Ogre::Light::LT_SPOTLIGHT);
 
// Configuramos su color (blanco)
faroA->setDiffuseColour(1.0, 1.0, 1.0);
faroA->setSpecularColour(1.0, 1.0, 1.0);
faroB->setDiffuseColour(1.0, 1.0, 1.0);
faroB->setSpecularColour(1.0, 1.0, 1.0);
 
// Mirando al frente, -z negativo, suponemos que es la dirección del coche.
faroA->setDirection(0, 0, -1);
faroB->setDirection(0, 0, -1);
 
// Creamos los subnodos para los focos
Ogre::SceneNode* nodoFaroA = nodoCoche->createChildNode("nodoFaroA");
Ogre::SceneNode* nodoFaroB = nodoCoche->createChildNode("nodoFaroB");
 
// Adjuntamos los focos a los nodos
nodoFaroA->attachObject(faroA);
nodoFaroB->attachObject(faroB);
 
// Posicionamos los focos de forma relativa al coche
// Faro derecho (desde nuestra perspectiva)
nodoFaroA->translate(1, 0.5, -1, Ogre::Node::TS_PARENT);
nodoFaroB->translate(-1, 0.5, -1, Ogre::Node::TS_PARENT);


Niebla y distancia de renderizado

El MMORPG Ryzom utiliza una niebla muy suave para limitar la distancia de renderizado de forma progresiva

Si echamos la vista atrás quizás recordemos que con los métodos setNearClipDistance y setFarClipDistance de la clase Camera podíamos configurar el frustrum para controlar la distancia a partir de la cual se comenzaba a renderizar (plano cercano) y a partir de la cual se dejaba de hacerlo (plano lejano). Si quieres repasar estos conceptos puedes hacerlo en el artículo Creación básica de escenas. En juegos que presenten escenarios enormes como suele ocurrir en el género RPG un equipo mediano no sea capaz de procesar una cola de renderizado de ese tamaño. En generaciones en los que la tecnología 3D aún era poco madura este problema era mucho más grave y los desarrolladores recurrían continuamente a la técnica de la niebla.

El survival horror Penumbra: Overture utiliza una niebla espesa y oscura para contribuir a su ambientación

Otros juegos utilizan la niebla como recurso a favor de la ambientación. Imagina un videojuego de terror en el que el personaje avanza por un pueblo maldito. Si existe una niebla muy intensa y oscura, la sensación de desasosiego en el jugador será muchísimo mayor. Si estás trabajando en un survival horror, una niebla oscura es un factor muy a tener en cuenta.

La niebla es un simple filtro que va ocultando progresivamente los objetos de la escena. Si acercamos la cámara a los objetos ocultos éstos irán mostrándose poco a poco. En cambio, si nos vamos alejando la niebla acabará por cubrirlos completamente. Ogre trabaja con la niebla de una forma muy sencilla. Lo único que hace es tomar el color del Viewport asociado a la cámara actual y aplica un degradado que comienza en 0 y acaba tomando el color completo de dicho Viewport. Por supuesto, los parámetros que definen el comportamiento de la niebla som completamente configurables y es lo que detallaremos a continuación.

Tipos de niebla

Ogre representa los tipos de niebla utilizando el enumerado Ogre::FogMode con 4 clases diferentes de niebla:

  • Ninguna: representada por el valor Ogre::FOG_NONE. Es el valor por defecto de la niebla, inexistente.
  • Lineal: representada por el valor Ogre::FOG_LINEAR. Es un tipo de niebla con una transición bastante suave. El valor de la niebla se va incrementando de forma lineal el función de la distancia a la cámara y de la densidad de la misma.
  • Exponencial: representada por el valor Ogre::FOG_EXP. El incremento del valor de la niebla de un punto a otro no es constante sino que cada vez es mayor (exponencial). Por ello, la transición es algo más brusca.
  • Exponencial al cuadrado: representada por el valor Ogre::FOG_EXP2. Sigue siengo un incremento exponencial a medida que se aleja de la cámara pero más radical que la clase anterior.

Configurando la niebla

En esta sección explicaremos los métodos necesarios para configurar un efecto de niebla en función de nuestros gustos y necesidades. En primer lugar, es necesario elegir un color para la niebla. Una vez elegido el color debemos aplicárselo al fondo del Viewport con el que estemos trabajando. Recordemos que la niebla es un filtro que se aplica de forma progresiva desde la cámara hasta el fondo (Viewport). Si el color del Viewport es distinto al de la niebla se producirá un efecto extraño no deseado. Para establecer el color del Viewport disponemos del método Viewport::SetBackgroundColour.

void Ogre::Viewport::setBackgroundColour(const ColourValue& colour);
  • const ColourValue& colour: color que adoptará el fondo del Viewport seleccionado.

Una vez hecho esto ya podemos indicarle a nuestro gestor de escena (SceneManager) que deseamos un efecto de niebla. Para ello existe el método SceneManager::setFog, cuenta con varios parámetros que detallaremos a continuación:

void Ogre::SceneManager::setFog(FogMode mode = FOG_NONE,
                                const ColourValue &colour = ColourValue::White,
                                Real expDensity = 0.001,
                                Real linearStart = 0.0,
                                Real linearEnd = 1.0);
  • FogMode mode: tipo de niebla deseada. Recordemos que existen 3 tipos de niebla que surtan algún efecto: FOG_LINEAR, FOG_EXP y FOG_EXP2. Por defecto se suprime la niebla.
  • const ColourValue &colour: color elegido para la niebla, debería ser el mismo que el fondo del Viewport.
  • Real expDensity: densidad de la niebla. Sólo tiene sentido en la niebla de tipo exponencial. Indica el factor por el que la niebla se hace cada vez más espesa.
  • Real linearStart: comienzo de la niebla, sólo tiene sentido en la niebla lineal. Desde la cámara a la distancia de comienzo la niebla no entra en acción.
  • Real linearEnd: fin de la niebla, sólo tiene sentido en la niebla lineal. A partir de esta distancia la niebla alcanzará una densidad 1 (completa, no se verá nada). Esto quiere decir que el gradiente de niebla abarca el intervalo que va desde la distancia de comienzo a la distancia de finalizado.

Configurar la niebla es harto sencillo aunque adjuntamos un pequeño fragmento de código que ilustre el proceso para que no haya lugar a dudas.

// Suponemos los siguientes objetos inicializados:
// Ogre::SceneManager* gestorEscena;
// Ogre::RenderWindow* ventana;
 
// Tomamos el primer Viewport
Ogre::Viewport* viewport = ventana->getViewport(0);
 
// Elegimos el color de la niebla (color muy claro)
Ogre::ColourValue colorNiebla(0.9, 0.9, 0.9);
 
// Establecemos el color de fondo del viewport
viewport->setBackgroundColour(colorNiebla);
 
// Establecemos la niebla (exponencial)
// Sólo tiene sentido indicar la densidad, obviamos las distancias de comienzo y fin
gestorEscena->setFog(Ogre::FOG_EXP, colorNiebla, 0.0001);

Niebla en varios Viewports

¿Qué ocurre si tenemos varios Viewports? Puede ocurrir que nuestro juego maneje varias cámaras al mismo tiempo en Viewports distintos como en el juego a pantalla dividida, por ejemplo. En ese caso será necesario iterar por todos los Viewports estableciendo su color de fondo. El siguiente fragmento de código ilustra este proceso:

// Suponemos los siguientes objetos inicializados:
// Ogre::SceneManager* gestorEscena;
// Ogre::RenderWindow* ventana;
 
// Elegimos el color de la niebla (color muy claro)
Ogre::ColourValue colorNiebla(0.9, 0.9, 0.9);
 
// Tomamos el número de Viewports
int numViewports = ventana->getNumViewports();
 
// Recorremos todos los Viewports
for (int i = 0; i < numViewports; ++i)
    ventana->getViewport(i)->setBackgroundColour(colorNiebla);
 
// Establecemos la niebla (lineal)
// Indicamos las distancias de inicio y de fin
gestorEscena->setFog(Ogre::FOG_LINEAR, colorNiebla, 0.0001, 50, 500);


SkyBoxes, SkyDomes y SkyPlanes

En la demo del plugin Speedtree se utiliza un fondo para representar el cielo de forma bastante acertada

La inmensa mayoría de videjuegos 3D en los que se muestran espacios abiertos utilizan un fondo con una textura plana para representar el cielo. Algunos están mejor conseguidos o producen sensaciones distintas pero, en definitiva, recurren a la misma técnica. Un cielo no tiene porqué ser una textura azul con nubes, puede referirse a un fondo con estrellas, cometas y galaxias. Como siempre, depende de tu juego. Ogre no podía ser menos y nos ofrece 3 técnicas diferentes para colocar cielos en nuestra aplicación 3D. Cada una tiene sus características propias y es más apropiada para determinadas situaciones. Lo mejor que puedes hacer si estás indeciso, es probar las 3, manipular sus parámetros y decidir cúal es el tipo de cielo que más favorece a tu videojuego.

En esta sección nos dedicaremos a exponer los distintos tipos de cielo (SkyBoxes, SkyDomes y SkyPlanes). Así mismo, aprenderemos a mostrarlos en Ogre y a modificar sus opciones.

SkyBoxes

Un SkyBox no es más que un cubo gigante que engloba a tu escena por completo. Es como una gran caja que contiene todos los elementos del juego con un papel interior que sirve de fondo. Para activar o desactivar el SkyBox de nuestra escena emplearemos el método SceneManager::setSkyBox.

void Ogre::SceneManager::setSkyBox(bool enable,
                                   const String& materialName,
                                   Real distance = 5000,
                                   bool drawFirst = true,
                                   const Quaternion& orientation = Quaternion::IDENTITY,
                                   const String& groupName = ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
  • bool enable: activa (true) o desactiva (false) el SkyBox.
  • const String& materialName: nombre del material a utilizar por el sistema de renderizado al mostrar el SkyBox. Normalmente será un cielo con nubes o un fondo estrellado.
  • Real distance: distancia desde la cámara a cada uno de los planos que conforman el SkyBox. Según la documentación oficial de Ogre, el valor por defecto suele dar buenos resultados.
  • bool drawFirst: true si el SkyBox se dibuja primero que el resto de la escena y viceversa.
  • const Quaternion& orientation: rotación del SkyBox, lo normal es mostrarlo sin rotación. Si se modifica poco a poco podrías simular el movimiento de las nubes.
  • const String& groupName: nombre del grupo que contiene el material indicado anteriormente.

El número de parámetros y de posibilidades podría asustar al principio pero manejar un SkyBox es extremadamente sencillo. Los parámetros de mayor interés son la distancia y el orden de renderizado. Si la distancia del SkyBox a la escena es demasiado pequeña puede que el fondo "se trague" algún elemento de la misma. Podríamos solucionar este problema indicándole al gestor de escena que renderize en primer lugar el SkyBox y después el resto de la escena. De esta manera jamás desaparecería ninguna entidad. No obstante, el SkyBox se renderizará al completo desaprovechando los algoritmos de oclusión de Ogre. La opción más eficiente es dibujar el SkyBox en último lugar y seleccionar una distancia de dibujado adecuada.

Vemos un ejemplo de uso de un SkyBox en el siguiente fragmento de código:

// Suponemos los siguientes objetos inicializados:
// Ogre::SceneManager* gestorEscena;
 
gestorEscena->setSkyBox(true, "SkyBox", 5000, false);

SkyDomes

El concepto del SkyDome es muy similar al del SkyBox. Un cubo gigante con una textura interna envuelve a la escena por completo. En esta ocasión, Ogre renderiza la textura del cubo con cierta curvatura, de forma que parece una esfera. Podría parecer que es una solución mucho mejor que la anterior pero hay que tener en cuenta que la zona inferior de la esfera no poseerá textura alguna. Esto implica que deberías tener algún tipo de suelo que impida al jugador ver el truco. Los SkyDomes funcionan bastante bien en situaciones en las que no existe niebla. Para activar o desactivar un SkyDome emplearemos el método SceneManager::setSkyDome.

void Ogre::SceneManager::setSkyDome(bool enable,
                                    const String& materialName,
                                    Real curvature = 10,
                                    Real tiling = 8,
                                    Real distance = 4000,
                                    bool drawFirst = true,
                                    const Quaternion& orientation = Quaternion::IDENTITY,
                                    int xsegments = 16,
                                    int ysegments = 16,
                                    int ysegments_keep = -1,
                                    const String& groupName = ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
  • bool enable: activa (true) o desactiva (false) el SkyDome.
  • const String& materialName: nombre del material con la textura para el interior de la esfera.
  • Real curvature: curvatura de la esfera.
  • Real tiling: número de veces que se repite la textura al renderizarla sobre el interior de la esfera. Si la textura es de baja resolución se precisa de un nivel de tiling mayor.
  • Real distance: distancia de la cámara al límite de la esfera. Es el radio de la esfera tomando la cámara como el centro.
  • bool drawFirst: true si el SkyDome se renderiza antes que el resto de la escena, false en caso contrario.
  • const Quaternion& orientation: rotación de la esfera.
  • int xsegments: número de segmentos en el eje x que tendrá la esfera.
  • int ysegments: número de segmentos en el eje y que tendrá la esfera.
  • const String& groupName: nombre del grupo al que pertenece el material del SkyDome.

¿Qué curvatura es la más apropiada? Si en tu juego aparecen escenarios muy abiertos lo mejor es elegir una curvatura limitada. En cambio, si sólo muestras pequeñas porciones del cielo, quizás en un FPS, lo más adecuado es inclinarse por curvaturas más elevadas. La relación entre la distancia y el orden de dibujado lo comentamos en el apartado de los SkyBoxes, en este caso se aplica de la misma forma.

Vemos un ejemplo de uso de un SkyDome en el siguiente fragmento de código:

// Suponemos los siguientes objetos inicializados:
// Ogre::SceneManager* gestorEscena;
 
gestorEscena->setSkyDome(true, "SkyDome", 30, 1.5, 5000, false);

SkyPlanes

Los SkyPlanes son muy diferentes a los SkyBoxes y SkyDomes ya que no utilizan cubos sino que se limitan a un simple plano texturizado (normalmente mirando hacia abajo). Los SkyPlanes son más eficientes que las dos técnicas anteriores, no obstante, funcionan en un menor número de ocasiones. Con escenarios muy abiertos podríamos ver que en el horizonte el cielo no converge con la tierra produciendo un efecto horrible. En cambio, si nuestras escenas tienen muros muy altos podría ser la técnica idónea. Para establecer un SkyPlane utilizamos el método SceneManager::setSkyPlane.

void Ogre::SceneManager::setSkyPlane(bool enable, 
                                     const Plane& plane,
                                     const String& materialName,
                                     Real scale = 1000,
                                     Real tiling = 10,
                                     bool drawFirst = true,
                                     Real bow = 0,
                                     int xsegments = 1,
                                     int ysegments = 1,
                                     const String& groupName = ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
  • bool enable: activa (true) o desactiva (false) el SkyPlane.
  • const Plane& plane: plano con la información para generar el SkyPlane.
  • const String& materialName: nombre del material que contendrá la textura para el plano.
  • Real scale: factor de escala aplicado al plano, dependiendo de las características de tu escena deberás ajustarlo de una forma u otra.
  • Real tiling: repetición de la textura a lo largo del plano, dependerá del tamaño del plano y de la textura.
  • bool drawFirst: si se indica true, el SkyPlane se dibujará antes que el resto de la escena y viceversa
  • Real bow: factor de curvatura del plano, es posible curvar ligeramente el plano para conseguir el efecto deseado.
  • int xsegments: segmentos del eje x que forman el plano.
  • int ysegments: segmentos del eje y que forman el plano.
  • const String& groupName: nombre del grupo al que pertenece el material.

Para finalizar, mostramos un ejemplo de uso de un SkyPlane.

// Suponemos los siguientes objetos inicializados:
// Ogre::SceneManager* gestorEscena;
 
// Creamos un plano mirando hacia abajo (eje Y)
Ogre::Plane plano(Ogre::Vector3::NEGATIVE_UNIT_Y, 1000);
 
// Activamos el SkyPlane
gestorEscena->setSkyPlane(true, plano, "SkyPlane");

Ejemplo

Diagrama de clases del ejemplo de luces, sombras y entorno
Ejemplo de iluminación, sombras y entorno
Pequeña aplicación que crea una escena con un personaje en un plano con varias luces interactivas

En este ejemplo vamos a hacer un repaso a grosso modo de todos los conceptos que hemos desgranado a lo largo del artículo. Tenemos la clase AplicacionOgre a la que ya estamos acostumbrados cuyo trabajo es iniciar Ogre y OIS. La clase EscenaSimple es la encargada de preparar los recursos, configurar el SceneManager, crear la cámara y disponer la escena. Nuestra escena está compuesta por un suelo representado por un plano y una textura, nuestro personaje y 4 luces de varios colores (rojo, azul, amarillo y verde). Aunque no encaje demasiado hemos dispuesto un SkyDome para abordar todo el contenido del artículo. Por último, tenemos niebla de carácter lineal activada.

El ejemplo está diseñado de forma que se pueda interactuar con él. A través del teclado podemos activar y desactivar los efectos como si de interruptores se tratase. A continuación se listan los controles:

  • s: alterna entre una sombra de tipo "modulative" y "additive". Es la mejor forma de ver la diferencia entre ambas.
  • d: activa o desactiva el SkyDome.
  • n: activa o desactiva la niebla.
  • 1: apaga o enciende la luz número 1 (roja).
  • 2: apaga o enciende la luz número 2 (azul).
  • 3: apaga o enciende la luz número 3 (verde).
  • 4: apaga o enciende la luz número 4 (amarilla).

Es muy recomendable que interactúes con la aplicación para comprobar de primera mano las consecuencias de cada elemento en la escena. Más tarde puedes modificar el código cambiando diversos parámetros. Por ejemplo, podrías emplear luces direccionales o de tipo foco en lugar de puntos de luz. Quizás sería interesante cambiar los parámetros de la niebla o sustituir el SkyDome por un SkyPlane.

Recuerda que debes preparar los plugins (pues no vienen incluidos en el paquete). Puedes encontrar más información sobre esto en "Creación de un entorno de trabajo multiplataforma". Los plugins necesarios son:

  • RenderSystem_GL
Captura del ejemplo de luces, sombras y entorno en ejecución


Conclusiones

En este extenso artículo hemos hecho un recorrido por las técnicas que ofrece Ogre para enriquecer y configurar parte del apartado visual de tus escenas. A partir de ahora podrás incorporar características tan interesantes como luz y sombras en tu videojuego. Podrás gestionar más sabiamente los recursos de la plataforma destino limitando la capacidad gráfica mediante una reducción del número de elementos que proyectan sombras. También podrás hacerlo poniendo un límite a la distancia de renderizado haciendo uso de la niebla. Los fondos son una técnica sencilla que te permitirán mejorar tus entornos sin muchas complicaciones.

Hemos repasado cientos de opciones para estos sistemas. Lo mejor que puedes hacer para comprenderlas todas es prácticar sin parar. Puedes empezar por el ejemplo que acompaña al artículo. Ejécutalo e interactúa con él. No tengas miedo por examinar el código, cuando lo comprendas puedes (y debes) modificarlo para añadir otras características o cambiar diversos parámetros.

Herramientas personales