Estos días he estado trasteando con el Rendermonkey para intentar entender mejor las ecuaciones de iluminación. He ido poco a poco traduciendo las ecuaciones a código (nada de copiar y pegar) y ahora lo entiendo mucho mejor.

Obtener la iluminación clásica de ambient + diffuse + specular ha sido sencillo. Y pasar los cálculos al pixel shader para hacer per-pixel lighting no dió mucho más trabajo. Sin embargo, yo quería hacer algo un poco más complejo, algo como Normal Mapping que es una técnica que se realiza con pocas líneas de código y da unos resultados excelentes. Aunque la desventaja es que exige un poco más de conocimiento matemático. Pero aún así resulta un gran shader para aprender técnicas que se aplicarán en otros shaders más complejos. El resultado es lo muestro aquí:

Normal Mapping

Sí, es nuestro querido weighted companion cube, aunque la textura no me quedó demasiado bien (en las caras superior e inferior, la repetición de la textura no encaja bien).

Empecemos por el principio. Para hacer normal mapping necesitamos unos requisitos iniciales:

  • Tenemos que comprender la ecuación de iluminación básica. Os recomiendo un tutorial que encontré que lo explica bastante bien.
  • El modelo debe tener en cada vérice, además de la normal, la binormal y la tangente. Básicamente forman el clásico eje XYZ pero para un vértice dado y sirve para saber la orientación de cada polígono.
  • Necesitamos el normal map que es una textura especial que almacena las normales.

El normal mapping sustituirá a la iluminación difusa, así que olvidaros de pasar la normal al pixel shader, porque emplearemos la normal del normal map. En mi caso, dicha textura tiene este aspecto:

No parece tener ningun sentido visualmente ¿no?. Resulta más fácil de ver la textura si separamos las capas por colores:

La componente roja muestra el modelo iluminado por la izquierda. La verde, si la luz se encuentra arriba. Evidentemente, haciendo un espejo con las texturas tenemos la iluminación por debajo y por la derecha así que no hace falta generarlas explícitamente. La componente azul nos muestra el relieve de la textura y no resulta especialmente significativo en el normal mapping. En el parallax mapping se hace un uso mucho más intenso de esta última componente para generar un falso relieve.

Necesitaremos la normal, binormal y tangente para poder situar la textura en el espacio 3D, porque la función tex2D no tiene eso en cuenta. Sin embargo no podemos establecer una textura en un mundo 3D, no tiene sentido, así que lo que se hace es transformar el resto de vectores (luz y posición de la cámara) de forma que la textura quede como si fuera el centro de la escena. Es decir, el polígono que estemos pintando será el origen de la escena, de esa forma podremos obtener la normal correcta.

Para transformar los vectores luz y posición se emplean la normal, binormal y tangente, que forman un espacio de coordenadas propio (una matriz) que al multiplicarla nos dará los vectores luz y posición en ese espacio de coordenadas. Tendremos que pasar esos vectores transformados al pixel shader que los usará igual que hasta ahora para generar el componente difuso y especular. La normal se extrae del normal mapping de la siguiente manera:

float3 normal = 2.0f * tex2D(Texture1, Input.TexCoord).rgb – 1.0f;

El resultado final (en un modelo un poco más espectacular) se muestra a continuación. La textura empleada para las normales está hecha con el plugin para Photoshop de NVidia, así que no es tan exacto como extrae el normal map de un modelo de alta definición.

Código fuente | Pieter Germishuys Blog