jueves, 27 de mayo de 2010

Bump Mapping

Después de tirarme una semana bastante agobiada implementando y entendiendo estoy, voy a hacer mi propio tutorial ahora que he conseguido que funcione y lo entiendo :P. Y antes de empezar os enseño el resultado para que veáis como mola :D.


Imaginaos un escenario de un juego en tres dimensiones con muchos elementos, y cada elemento con sus detalles. Cada uno de los objetos de este escenario, en su geometría, tendrá todos los detalles representados como triángulos. Por lo que todo el escenario podría tener miles de millones de triángulos para dibujar. Cuando nos moviésemos por este escenario, todo iría muy lento porque habría que pintar en la pantalla todos los triángulos, asignarles color y textura, iluminación, ... En resumen que sería imposible jugar de una fluida.

Una solución sería disminuir la complejidad geométrica de los objetos, es decir, hacer que tengan menos triángulos. El problema de esta solución es que cuantos menos triángulos dibujes menos detalles se verán en la escena, pero todo iría mucho más fluido ...

En este caso se puede aplicar una textura, de manera que se dé una apariencia realista al objeto. Aunque aplicando sólo la textura el objeto no se va a ver igual que si dibujásemos toda la geometría, se verá todo un poco plano ...

Pero ¿y si pudiésemos simular un objeto complejo con una geometría simple y una textura? Justo esto es lo que hace el bump mapping.

El bump mapping lo que hace es modificar la iluminación en cada uno de los puntos del objeto, de tal manera que el objeto parezca mucho más complejo aunque no lo sea :P.

Para modificar la iluminación en cada punto del objeto habrá que tener en cuenta la posición de la luz, la normal cada punto del objeto y la normal en cada punto de la textura. Como veis nos estamos moviendo en tres sistemas diferentes, en el mundo, en el espacio del objeto y en el espacio de textura (tangent space), respectivamente. Por lo que habrá que pasar de uno a otro para poder aplicar las perturbaciones en las normales del objeto.

Más o menos os voy a definir por pasos lo que habría que hacer y como hacerlo en cada caso:
  1. Generar el mapa de normales de la textura. En mi caso, no lo he generado en el programa sino que haciendo uso del programa Crazybump he generado una imagen con el mapa de normales de la textura que vaya a usarse.

    Este mapa de normales también se podría generar de forma automática de la siguiente manera:

    Si tenemos una imagen de (w, h) pixeles, y tenemos el heighmap de la textura, básicamente la textura en blanco y negro podemos calcular el mapa de normales de la siguiente manera:

    t = (1, 0, heighmap[i+1][j] - heigmap[i-1][j]);
    b = (0, 1, heighmap[i][j+1] - heigmap[i][j-1]);
    n = t x b (producto vectorial de t por b)


    n estará en el rango [-1, 1], así que para poder codificarlo como imagen habrá que pasarlo al rango [0, 1] de la siguiente manera:

    n_compress = n/2 + 0.5;

    Pero por eficiencia decidí crearla fuera del programa.

    Este mapa de normales, el rango de valores variará de [-1,1]. Pero como se almacena en una imagen este rango irá de [0,1]. Por lo que cuando se vayan a usar los valores del mapa de normales habrá que descomprimirlos de la siguiente manera:

    n = 2*(n_compress-0.5);

  2. Pasar de coordenadas de mundo a coordenadas de objeto es algo fácil, ya que ambos mundo son mundos en 3D. El problema viene al pasar del espacio del objeto al espacio de textura, ya que es pasar de 3D a 2D. Para realizar el cambio de uno a otro será necesario crear la matriz TBN y su inversa. En la siguiente imagen vemos un esquema de como sería el espacio de textura:


    donde S es la tangente en el punto en la dirección del eje X, T es la tangente en el punto en la dirección del eje Y, y N indica el eje Z, que sale hacia fuera. Cada vértice de los triángulos que forman el objeto le corresponde su propio espacio de textura. De esta forma, si ponemos los ejes en forma de matriz obtendremos la matriz TBN (Tangent Binormal Normal) (que corresponden a los ejes S, T y N, respectivamente). La forma de calcular esta matriz es:

    Tenemos un triángulo con vértices V1, V2, V3 y coordenadas de textura asociadas a esos vértices C1, C2, C3.

    C2C1T = C2.x - C1.x;
    C2C1B = C2.y - C1.y;
    C3C1T = C3.x - C1.x;
    C3C1B = C3.y - C1.y;
    V2V1 = V2 - V1;
    V3V1 = V3 - V1;

    T= (C3C1B * V2V1 - C2C1B * V3V1)/(C2C1T*C3C1B - C3C1T*C2C1B);
    B= (C3C1T * V2V1 - C2C1T * V3V1)/(C2C1T*C3C1B - C3C1T*C2C1B);
    N = TxB (producto vectorial de T por B)


    Para calcular la inversa de la matriz TBN se haría:

    A1 = BxN;(producto vectorial de B por N)
    A2 = NxT;(producto vectorial de N por T)
    A3 = TxB;(producto vectorial de T por B)
    denominador = ((T.x*B.y*N.z - T.z*B.y*N.x) + (B.x*N.y*T.z - B.z*N.y*T.x) + (N.x*T.y*B.z - N.z*T.y*B.x));
    Tinv = (A1.x, -A1.y, A1.z) / denominador;
    Binv = (-A2.x, A2.y, -A2.z) / denominador;
    Ninv = (A3.x, -A3.y, A3.z) / denominador;


  3. Calcular el nuevo color para cada uno de los puntos de la imagen de la siguiente manera:

    I = D1*Dm*clamp(L·N, 0, 1)

    donde D1 es el color difuso de la luz,
    Dm es el color difuso del material del objeto,
    L es el vector que va desde un punto de la superficie a la luz,
    N es la normal de la superficie en un punto.
Esto no va todo implementado en openGL sino que para agilizar se ha implementado en GPU (tarjeta gráfica) usando el lenguaje de programación CG. Si queréis saber algo más de CG lo suyo es que lo miréis en la página de nvidia.

Habrá que generar dos programitas uno para los vértices y otro para los fragmentos (puntos que pertenecen al objeto).

En el programa de vértices lo que haremos es transformar la posición de la luz al espacio de textura.

vlight = vlightPosition - position.xyz; //así pasamos la luz al espacio del objeto
vlight.xyz = mul(TBNMatrix, vlight);

El programa para los fragmentos lo que hace es calcular el color en cada punto.

vlight = normalize(vlight);
vNormal = 2.0f * (tex2D(normalTexture, normalCoords).rgb - 0.5); //descomprimo el valoor del normal map
colorOUT.rgb = fLightDiffuseColor * tex2D(baseTexture, texCoords).rgb * saturate(dot(vlight, vNormal)); //calculamos el color final, sturate = clamp

Lo que sí os voy a decir es como añadir a vuestro programa en opengGL el vertex y el fragment shaders:
  1. Inicialización

    bool esfera::InitCG(){
    // cg context

    g_context = cgCreateContext();


    // Find the best matching profile for the fragment shader

    g_fragmentProfile = cgGLGetLatestProfile(CG_GL_FRAGMENT);

    cgGLSetOptimalOptions(g_fragmentProfile);

    if (g_fragmentProfile == CG_PROFILE_UNKNOWN){
    return false;

    }


    // Find the best matching profile for the vertex shader

    g_vertexProfile = cgGLGetLatestProfile(CG_GL_VERTEX);

    cgGLSetOptimalOptions(g_vertexProfile);


    if (g_vertexProfile == CG_PROFILE_UNKNOWN) {
    return false;

    }


    // Create the fragment program.

    g_fragmentProgram = cgCreateProgramFromFile(g_context, CG_SOURCE, "FragmentShader.cg", g_fragmentProfile, "main", 0);


    if (!g_fragmentProgram)
    return false;


    // Load the fragment program

    cgGLLoadProgram(g_fragmentProgram);


    // Create the vertex program

    g_vertexProgram = cgCreateProgramFromFile(g_context, CG_SOURCE, "VertexShader.cg", g_vertexProfile, "main", 0);


    if (!g_vertexProgram)
    return false;


    // Load the vertex program

    cgGLLoadProgram(g_vertexProgram);


    // Get the parameters which we can pass to the vertex and fragment shaders.

    g_modelViewMatrix = cgGetNamedParameter(g_vertexProgram, "modelViewProjMatrix");

    g_lightPosition = cgGetNamedParameter(g_vertexProgram, "vLightPosition");

    g_lightDiffuseColor = cgGetNamedParameter(g_fragmentProgram, "fLightDiffuseColor");


    return true;

    }


  2. Uso

    // Enable the vertex and fragment profiles and bind the vertex and fragment programs
    cgGLEnableProfile(g_vertexProfile);

    cgGLEnableProfile(g_fragmentProfile);


    cgGLBindProgram(g_vertexProgram);

    cgGLBindProgram(g_fragmentProgram);


    // Set the "modelViewProjMatrix" parameter in the vertex shader to the current concatenated

    // modelview and projection matrix

    cgGLSetStateMatrixParameter(g_modelViewMatrix,
    CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY);


    // Set the light position parameter in the vertex shader

    cgGLSetParameter3f(g_lightPosition, pos[0], pos[1], pos[2]);


    // Set the diffuse of the light in the fragment shader

    cgGLSetParameter3f(g_lightDiffuseColor, color_difusa[0], color_difusa[1], color_difusa[2]);


    //codigo para habilitar las texturas


    //codigo para dibujar el objeto


    cgGLDisableProfile(g_vertexProfile);

    cgGLDisableProfile(g_fragmentProfile);


    glActiveTextureARB(GL_TEXTURE0_ARB);
    glDisable(GL_TEXTURE_2D);


    glActiveTextureARB(GL_TEXTURE1_ARB);

    glDisable(GL_TEXTURE_2D);


  3. Destrucción de los parámetros

    void esfera::DestroyCG(){
    // Destroying the CG context automatically destroys all attached CG programs

    cgDestroyContext(g_context);

    }


El código lo podeis descargar de aquí.

El programa está hecho en linux, más concretamente en Kubuntu 9.10. Para poder compilar el programa y que todo funcione hace falta que instaleis lo siguiente:
  • CG toolkit, que se puede descargar aquí.
    También se puede instalar mediante apt-get de la siguiente forma:
    sudo apt-get install nvidia-cg-toolkit

  • Tener instalado openGL. Si teneis una tarjeta gráfica nvidia al instalar los drivers instala todo lo necesario para usar openGL. Si no teneis tarjeta gráfica o no sabeis como instalar openGL, podeis usar MESA que es un emulador.

  • Tener instalado la librería gráfica para el manejo de ventanas glut:
    sudo apt-get install freeglut3 freeglut3-dbg freeglut3-dev ftgl-dev gle-doc glut-doc glutg3 glutg3-dev

  • Tener instalado las librerías para manipulación de imágenes SDL:
    apt-get install libsdl1.2debian apt-get install libsdl1.2-dev apt-get install libsdl-image1.2 apt-get install libsdl-image1.2-dev apt-get install libsdl-mixer1.2 apt-get install libsdl-mixer1.2-dev apt-get install libsdl-ttf2.0-0 apt-get install libsdl-ttf2.0-dev
Fuentes:

2 comentarios:

  1. Joer qué guay... estas cosas son las que nos tendrían que haber enseñado en la facultad y ya lo hubiéramos flipado del todo. Y lo más triste es que algunas cosas me han sonado a chino ^^u (quitando las mierdas de Linux :P) con lo que yo era pa estas cosas :P
    En fin, ya nos enseñarás más cosillas cuando lo tengas avanzado.

    ResponderEliminar
  2. Pues si, esto está más chulo que la grua de DAC :P. Y lo de chino no te preocupes que a mi la primera vez que lo leí, y la segunda y alguna más tampoco me enteraba de mucho :S

    ResponderEliminar