Concetpos fundamentales de la superposición de texturas
En el desarrollo de aplicaciones gráficas con OpenGL, es común enfrentarse a la necesidad de superponer una imagen pequeña (como un logotipo o una marca de agua) sobre una textura de fondo que ocupa toda la pentalla. Este efecto se logra definiendo diferentes conjuntos de datos de vértices para cada elemento y ejecutando múltiples llamadas de dibujo en un orden específico.
Para implementar esta técnica de manera eficiente, utilizaremos diferentes Objetos de Matriz de Vértices (VAO) y Objetos de Búfer de Vértices (VBO). El primer conjunto representará el plano de fondo, mientras que el segundo definirá la posición y el tamaño del elemento superpuesto en el espacio de coordenadas normalizadas (NDC).
Configuración del sombreador de vértices (Vertex Shader)
El sombreador de vértices se encarga de transformar las posiciones de los puntos y pasar las coordenadas de textura al siguiente paso del pipeline gráfico.
#version 330 core
layout (location = 0) in vec3 posicionAtributo;
layout (location = 1) in vec2 coordTexturaAtributo;
out vec2 InterpTexCoord;
void main()
{
gl_Position = vec4(posicionAtributo, 1.0);
InterpTexCoord = coordTexturaAtributo;
}
Lógica del sombreador de fragmentos (Fragment Shader)
Para distinguir entre la textura de fondo y el elemento superior dentro de un mismo programa de sombreado, empleamos una variable de tipo uniform que actúa como selector.
#version 330 core
out vec4 ColorSalida;
in vec2 InterpTexCoord;
uniform sampler2D unidadTexturaFondo;
uniform sampler2D unidadTexturaLogo;
uniform int modoRenderizado;
void main()
{
if(modoRenderizado == 0)
{
ColorSalida = texture(unidadTexturaFondo, InterpTexCoord);
}
else
{
ColorSalida = texture(unidadTexturaLogo, InterpTexCoord);
}
}
Implementación en C++ y OpenGL
A continuación, se presenta la lógica para configurar los búferes y el ciclo de renderizado. Se han definido dos mallas cuadradas: una que abarca el rango [-1, 1] y otra más pequeña ubicada en la esquina superior derecha.
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#include <iostream>
// Variables globales para la gestión de recursos
unsigned int vaoFondo, vaoOverlay;
unsigned int texFondoID, texOverlayID;
int programaID;
void prepararGeometria() {
float verticesFondo[] = {
// Posiciones // Coordenadas de Textura
1.0f, 1.0f, 0.0f, 1.0f, 1.0f,
1.0f, -1.0f, 0.0f, 1.0f, 0.0f,
-1.0f, -1.0f, 0.0f, 0.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 0.0f, 1.0f
};
float verticesOverlay[] = {
// Posiciones // Coordenadas de Textura
0.95f, 0.95f, 0.0f, 1.0f, 1.0f,
0.95f, 0.60f, 0.0f, 1.0f, 0.0f,
0.50f, 0.60f, 0.0f, 0.0f, 0.0f,
0.50f, 0.95f, 0.0f, 0.0f, 1.0f
};
unsigned int indices[] = { 0, 1, 3, 1, 2, 3 };
// Configuración VAO Fondo
glGenVertexArrays(1, &vaoFondo);
unsigned int vboF, eboF;
glGenBuffers(1, &vboF);
glGenBuffers(1, &eboF);
glBindVertexArray(vaoFondo);
glBindBuffer(GL_ARRAY_BUFFER, vboF);
glBufferData(GL_ARRAY_BUFFER, sizeof(verticesFondo), verticesFondo, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboF);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// Configuración VAO Overlay
glGenVertexArrays(1, &vaoOverlay);
unsigned int vboO, eboO;
glGenBuffers(1, &vboO);
glGenBuffers(1, &eboO);
glBindVertexArray(vaoOverlay);
glBindBuffer(GL_ARRAY_BUFFER, vboO);
glBufferData(GL_ARRAY_BUFFER, sizeof(verticesOverlay), verticesOverlay, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
}
unsigned int cargarTextura(const char* ruta, int unidad) {
unsigned int id;
glGenTextures(1, &id);
glActiveTexture(GL_TEXTURE0 + unidad);
glBindTexture(GL_TEXTURE_2D, id);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
int w, h, canales;
stbi_set_flip_vertically_on_load(true);
unsigned char* data = stbi_load(ruta, &w, &h, &canales, 0);
if (data) {
GLenum formato = (canales == 4) ? GL_RGBA : GL_RGB;
glTexImage2D(GL_TEXTURE_2D, 0, formato, w, h, 0, formato, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
stbi_image_free(data);
return id;
}
void ejecutarRenderizado() {
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(programaID);
// Dibujar Fondo
glUniform1i(glGetUniformLocation(programaID, "modoRenderizado"), 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texFondoID);
glBindVertexArray(vaoFondo);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// Dibujar Overlay (Logotipo)
glUniform1i(glGetUniformLocation(programaID, "modoRenderizado"), 1);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, texOverlayID);
glBindVertexArray(vaoOverlay);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}
Consideraciones sobre el orden de dibujo
El orden en el que se ejecutan las llamadas glDrawElements es crítico. OpenGL renderiza los objetos secuencialmente, lo que significa que el último objeto dibujaod aparecerá sobre los anteriores si se encuentran en la misma posición del eje Z. En este ejemplo, el fondo se dibuja primero para establecer la base de la escena, seguido por el elemento de superposición.
Es importante notar que, aunque este método permite posicionar imágenes sobre otras, el manejo de la transparencia requiere la activación de GL_BLEND y la configuración adecuada de la función de mezcla (glBlendFunc), además de asegurar que las texturas cargadas posean un canal alfa válido.