Texuture Atlas
Buenas a tod@s! En nuestro último post hablamos sobre los decals o calcomanías. Ahora voy a contaros mi experiencia con Brakeza3D sobre OpenGL. En este camino, me he visto en la necesidad de trabajar con múltiples texturas en el FragmentShader de GLSL. Hoy voy a hablaros de cómo crear un Texture Atlas.
A continuación y a modo de ejemplo, podéis ver un TextureAtlas donde agrupo todas las texturas de Ligthmapping existentes para un mapa.
Antiguamente, es decir, con OpenGL y su “fixed pipeline”, lo habitual sería cambiar de unidad de textura en cada “draw call” que hacemos a la GPU. Pero con la nueva pipeline programable, esto no es nada práctico, ya que en el momento de texturizar, ya habremos creado nuestro VBO al completo para los vértices de la geometría estática, por tanto debemos de resolver el texturizado del VBO al completo, sin salirnos del shader.
También es posible aproximarse al problema mediante “Arrays de Texturas”, una funcionalidad nativa de OpenGL, pero que tampoco está exenta de problemas, ya que los arrays de texturas, me obligan a preocuparme de garantizar que todas las texturas son del mismo tamaño.
En mi caso concreto, no puedo garantizar que todas las texturas tengan el mismo tamaño, asi que opto por implementar un algoritmo de TextureAtlas que me proporcione la información suficiente para llegar al FragmentShader y poder calcular correctamente las texturas de coordenada UV dentro del TextureAtlas
Algoritmo de creación TextureAtlas
La idea es disponer de un algoritmo al que arrojarle imágenes 2D y que el TextureAtlas sea suficientemente inteligente como para encajar esta imagen en el primer hueco disponible para ella o notificarnos si no hay espacio para la misma.
Estoy seguro que por la red puedan existir algoritmos más optimizados que éste que voy a presentar, el cual es una especie de “fuerza bruta” revisando pixel a pixel. Aunque el proceso apenas lleva unos segundos y en Brakeza3D lo resuelvo en tiempo de carga, lo óptimo sería tener pre-cacheados (es decir, ya generados a fichero), estos TextureAtlas. Asi se hace en Quake2, por ejemplo.
Para ello crearemos un buffer que usaremos como máscara para averiguar cuando un pixel está ocupado dentro de nuestro TextureAtlas. De esta manera y con unos sencillos cálculos iremos pixel a pixel garantizando que hay espacio.
A continuación el código más relevante del TextureAtlas
...
bool *mask = new bool[total_width * total_height];
...
struct TextureAtlasImageInfo {
std::string name;
float x;
float y;
float width;
float height;
};
bool TextureAtlas::addTexture(Texture *texture) {
SDL_Surface *texture_surface = texture->getSurface();
int texw = texture_surface->w;
int texh = texture_surface->h;
for (int y = 0 ; y < total_height ; y++) {
for (int x = 0 ; x < total_width ; x++) {
if ( this->checkForAllocate(x, y, texw, texh ) ) {
TextureAtlasImageInfo t_info;
t_info.name = texture->getFilename();
t_info.x = (float) x;
t_info.y = (float) y;
t_info.width = (float) texw;
t_info.height = (float) texh;
textures_info.push_back( t_info );
textures.push_back( texture );
allocateMask(x, y, texw, texh);
SDL_Rect r;
r.x = x;
r.y = y;
r.w = texw;
r.h = texh;
SDL_BlitSurface(
texture_surface,
NULL,
atlas_surface,
&r
);
return true;
}
}
}
return false;
}
void TextureAtlas::allocateMask(int xpos, int ypos, int width, int height)
{
int baseOffset = ypos * total_width + xpos;
for (int y = 0 ; y < height ; y++) {
for (int x = 0 ; x < width ; x++) {
int localIndex = y * width + x;
int globalIndex = baseOffset + localIndex;
mask[ globalIndex ] = true;
}
baseOffset += total_width - width;
}
}
Mapeo UV en AtlasTexture
Una vez disponemos de la información de nuestro TextureAtlas, es decir, disponemos de la posición X e Y de cada tile, además de su ancho y alto, estamos en disposición de remapear en el FragmentShader las UV de un triángulo dentro del atlas:
// Atlas Texture Re-Mapping
float texturePosX = fragment_lightmap_atlas[0]; // x
float texturePosY = fragment_lightmap_atlas[1]; // y
float textureSizeW = fragment_lightmap_atlas[2]; // width
float textureSizeH = fragment_lightmap_atlas[3]; // height
vec2 sizeT = vec2( textureSizeW / TEXTURE_ATLAS_W, textureSizeH / TEXTURE_ATLAS_H );
vec2 offsetT = vec2( texturePosX / TEXTURE_ATLAS_W, texturePosY / TEXTURE_ATLAS_H );
vec2 new_uv = offsetT + fragment_lightmap_uv * sizeT;
Problemas relacionados con TextureAtlas
El uso de TextureAtlas no está exento de problemas con los que lidiar. Una cuestión importante es tener en cuenta que OpenGL no podrá realizar sus interpolaciones para el modo GL_REPEAT adecuadamente cuando usamos TextureAtlas.
Los cálculos de OpenGL para GL_REPEAT se basan en las medidas del tamaño de la textura al completo, no de un subconjunto de la misma. Por tanto debemos evitar el TextureAtlas si nuestras UV se encuentran fuera del rango [0, 1].
Otra consideración a tener en cuenta, es el denominado “texture bleeding“. Si dentro de nuestro TextureAtlas las texturas se encuentran muy juntas, es posible que el FILTRO (por ejemplo filtro BILINEAR) seleccionado para texturización, llegue a elegir píxeles que pertenecen a otra textura. Por esto es conveniente dejar un margen de separación entre nuestras texturas dentro del propio TextureAtlas.