lunes, 10 de diciembre de 2012

Concurso: planetas fotorealistas

En esta entrada voy a plantear un concurso. Los premios serán el consabido café y un regalo sorpresa por definir (cuya característica más probable será que se pueda encontrar en una dollar store).

Antes de ir a los detalles, una breve introducción.

Siempre me ha parecido curioso, que siendo los planetas uno de los objetos aparentemente más sencillos de pintar con un ordenador, la mayoría de las imágenes 3D de planetas que he visto, no se parecen gran mucho a la realidad. Empezando por el programa Celestia, que lo hace (bastante bien) en tiempo real:


Y terminando por las más elaboradas imágenes generadas con programas de renderizado 3D. Estas requieren más tiempo de proceso y suelen incluir complicados efectos atmosféricos y varias texturas superpuestas).


Los resultados, aunque atractivos, se parecen poco a la realidad:


En general, parece que la tendencia suele ser hacer la atmósfera más grande de lo que es para que resulte más llamativa, hacer las nubes más transparentes para que se vean mejor los continentes, dar un poco de luz ambiente para mostrar la cara oculta y, algo que se ve mucho últimamente, hacer visible la luz artificial en las partes oscuras.

Dicho esto, vamos a por el concurso. Se trata de mejorar un programa base en C++ que yo os proporciono. Este programa incluye una rutina de renderizado sencilla, pero fácilmente extendible. No usa OpenGL ni requiere grandes conocimientos de C++ o programación gráfica. También incluye capacidades de movimiento de la cámara y tres cuerpos celestes (Sol, Tierra y Luna).

¿En qué pueden consistir las mejoras? he pensado en tres categorías:

1. Mejorar la rutina de pintado de forma que los planetas sean lo más realistas posibles.

Cuando digo "realistas" me refiero a que parezcan realistas. No es necesario que simulen exactamente la costa terrestre o los cráteres de la Luna. Deben ser lo suficientemente parecidos para que los planetas sean reconocibles a simple vista, pero dejo espacio por si alguien quiere meterse en cosas de generación algorítmica de texturas y cosas similares. Como este es un tema subjetivo, al final habrá una votación para decidir los resultados.

2. Implementar la física de una nave espacial.

En el código actual he incluído, de forma muy sencilla, el movimiento de la cámara/nave. Mejorar esta parte es bastante sencillo, pero hacerlo bien no. El código incluye una pequeña librería de operaciones entre vectores y matrices 3D, así que casi todo el trabajo es de modelado físico.

3. Aceleración del código.

El código, tal y como está, va a unos 12 FPS en una situación típica. Esto empeorará con rutinas de pintado más complicadas, pero también hay mucho espacio para la optimización. Inicialmente me concentrE en que el programa fuese fácil de entender, pero a partir de ahora voy a trabajar en la optimización y daré actualizaciones al que las quiera; pero cosas como usar la GPU no entran de momento en mis planes. También es posible mejorar los algoritmos en formas que no se me ocurran a mi.

Ejemplo del programa tal y como está. Para manejarlo usar cursores, control izquierda, barra espaciadora y enter.
Teniendo en cuenta las estadísticas del foro y lo ocupados que solemos estar, no me hago muchas ilusiones de que alguien quiera participar en el concurso, pero eso no me detiene. Como fecha tope voy a poner el 28F del 2013.

El proyecto se puede descargar desde aquí. Incluye un archivo de proyecto para code:blocks además de makefiles para Linux y Windows. Está pensado para ser compilado con gcc, pero no dudo de vuestras habilidades para convertirlo a cualquier otro compilador (o, incluso, otro lenguaje). Eso sí, es necesario tener la librería SFML 1 instalada (en Ubuntu se puede hacer desde el Software center).

Una breve descripción de las rutinas

El programa tiene un par de módulos auxiliares donde se definen las clases usadas por el programa: TPlanet, TCamera, TRay y TSolarSystem. Las tres primeras son más o menos evidentes, la cuarta es simplemente un agregado de planetas con (de momento) un sólo Sol.

La parte interesante del programa se encuentra en main.cpp, que es un programa de sólo 180 líneas. Consta de sólo dos funciones, la rutina de pintado:


void Render(sf::Image &img, TCamera const &Cam, TSolarSystem const &SolarSystem)
{
  const int W=img.GetWidth ();
  const int H=img.GetHeight();

  static const uint32_t clBlack = ColorToUint(sf::Color(0,0,0));

  uint32_t *ptr = (uint32_t *)img.GetPixelsPtr();
  for (int j=0;j<H;j++)
  {
    for (int i=0;i<W;i++)
    {
      // For every pixel (i,j) in the image...

      uint32_t col=clBlack, c;
      Vector3d n;
      double dist, lastdist=1e30;

      // Computes the ray associated to each pixel.
      const TRay ray=Cam.Ray(i,j);

      for (int k=0;k<SolarSystem.Count;k++)
      // For every planet...
      {
        const TPlanet &planet=*(SolarSystem.Planets[k]);

        // If planet k intersects ray...
        if (planet.Intersects(ray, n, dist))
        {
          c=planet.Color(n, SolarSystem.Sun);
          if (dist<lastdist)
          // If this intersection is closer than the last...
          {
            col=c;
            lastdist=dist;
          }
        }
      }

      // Deals with the Sun in a slightly different way.
      if (SolarSystem.Sun.Intersects(ray, n, dist))
      // If Sun intersects ray...
      {
        c=ColorToUint(sf::Color(255,255,240));
        if (dist<lastdist)
        // If this intersection is closer than the last...
        {
          col=c;
          lastdist=dist;
        }
      }

      // Sets the collor of pixel (i,j).
      *ptr=col;

      ptr++; // Next pixel.

    }
  }
}


Esta función hace algo muy sencillo: Existe una cámara asociada a la pantalla y por cada píxel de esa imagen crea un rayo (TRay) de luz y comprueba si intersecta algún planeta. En el caso de que haya varios planetas que intersecten el mismo rayo, se queda con el más cercano y obtiene el color de ese píxel. Si el rayo no intersecta ningún planeta el píxel será negro.

La clase TPlanet implementa la rutina Intersects(ray, n, dist) que devuelve la distancia al punto de intersección más cercano a la cámara y el vector normal a la superficie del planeta en ese punto. TPlanet también implementa una versión básica de la función Color que devuelve el color del píxel en el punto de intersección.

El siguiente gráfico muestra la situación de forma más clara:


La parte del manejo de la nave también se encuentra en main.cpp y es la siguiente:

    // Process events
  sf::Event Event;
  while (App.GetEvent(Event))
  {
    rot = Vector3d(0.0,0.0,0.0);

    if (Event.Type == sf::Event::KeyPressed)
    {
       if (Event.Key.Code==277) acc=-10000000.0;    // Acceleration
       if (Event.Key.Code==257) acc= 10000000.0;    // Deceleration
       if (Event.Key.Code==293) rot += 0.02*Cam.xv; // Up
       if (Event.Key.Code==294) rot -= 0.02*Cam.xv; // Down
       if (Event.Key.Code==291) rot +=  0.2*Cam.zv; // Left
       if (Event.Key.Code==292) rot -=  0.2*Cam.zv; // Right
       if (Event.Key.Code==278) vel = Vector3d(0.0,0.0,0.0); // Stops movement.
    }

    if (Event.Type == sf::Event::KeyReleased)
    {
       if (Event.Key.Code==277) acc= 0.0;
       if (Event.Key.Code==257) acc= 0.0;
    }

    // Close window : exit
    if (Event.Type == sf::Event::Closed) App.Close();
  }

  // Camera/ship movement.
  vel=vel+(acc*dt)*Cam.zv;
  Cam.Pos+=dt*vel;
  if (norm(rot)>0) Cam.Rotate(unitary(rot), dt*norm(rot));


Que como se ve, no tiene mucha complicación y es bastante cutre.

No hay comentarios:

Publicar un comentario