Texturas OpenGL Moderno

Una textura es una imagen que puede ser aplicada a un objeto 3D para darle mayor realismo, OpenGL maneja dos tipos de texturas, las texturas procedurales, son calculadas mediante algoritmos y las texturas de imagen, que son aplicadas a partir de un archivo de imagen, en este tutorial nos enfocaremos en las texturas de imagen.

UV Mapping


Es una técnica que permite corresponder cada vértice del objeto 3D con una coordenada de la imagen, se manejan imágenes cuadradas con tamaño igual a una potencia de dos (512, 1024, 2048, etc.), las coordenadas de la imagen están entre 0 y 1 en los ejes X y Y, en las texturas estas coordenadas son llamadas U y V respectivamente.

uv mapping opengl
Vemos en la imagen como una textura 2D y sus coordenadas UV son correspondidas con los vértices de un cuadrado y un triángulo, ahora procederemos a ver esta aplicación de texturas en código C++.

Cargar imagen BMP


Lo primero que necesitamos es cargar la correspondiente imagen, usaremos el formato BMP por ser bastante simple, crearemos un cargador para este tipo de archivos, aunque existen gran variedad de librerías externas que nos permiten manipular gran cantidad de formatos de imagen.

ifstream file = ifstream(image.c_str(), ios::in | ios::binary);

if (!file.is_open()) {
	cout << "No se a podido abrir la imagen: " << image << endl;
	return 0;
}

char header[54];

if (file.read(header, 54)) {
	int dataPos = *(int*)&(header[0x0A]);
	int imageSize = *(int*)&(header[0x22]);
	int width = *(int*)&(header[0x12]);
	int height = *(int*)&(header[0x16]);

	char* data = new char[imageSize];

	file.read(data, imageSize);
	file.close();
}

Creamos un fstream y leemos la imagen, los primeros 54 bytes del archivo contienen información referente a la imagen, ancho, alto, tamaño en bytes, etc., una vez tenemos la información leemos los datos y los almacenamos en la variable char* data.

Crear Textura OpenGL

El proceso de creación de una textura en OpenGL es bastante similar al utilizado en la creación de buffer que vimos en el tutorial vertex data, debemos crear la textura, enlazarla, configurarla y rellenarla con los datos adecuados.

// Crear un ID para la textura OpenGL
GLuint textureID;
glGenTextures(1, &textureID);

// Enlazar la textura recien creada
// GL_TEXTURE_2D la textura es un arreglo de pixeles de 2 dimensiones
glBindTexture(GL_TEXTURE_2D, textureID);

// Crear la textura
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_BGR, GL_UNSIGNED_BYTE, data);

Cuando en rango de las coordenadas UV está fuera de (0, 1) podemos usar la función glTexParameteri para controlar el comportamiento de la textura, usando GL_TEXTURE_WRAP_S para el comportamiento horizontal y GL_TEXTURE_WRAP_T para el vertical, por último indicaríamos uno de los cuatro modos existentes: GL_REPEAT, GL_MIRRORED_REPEAT, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_BORDER.

texture wrapping

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_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

Vemos también como podemos cambiar GL_TEXTURE_MAG_FILTER y GL_TEXTURE_MIN_FILTER al valor GL_LINEAR esto indica cómo deben calcularse los pixel de la imagen cuando esta se agranda o reduce para adaptarse a las coordenadas indicadas.

Modificamos el vertex shader para recibir el buffer de coordenadas UV y luego enviarlo al fragament shader.

layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec2 vTexture;

out vec2 texCoord;

void main()
{
     gl_Position = vPosition;
     texCoord = vTexture; 
}

El fragment shader recibe las coordenadas UV y la textura, y las aplica a cada pixel para producir la correspondiente salida.

in vec2 texCoord;
out vec4 color;

uniform sampler2D myTexture;

void main()
{
       color = texture(myTexture, texCoord);
}

Para finalizar vemos el resto del código, como lo venimos haciendo usamos el método, onstart para inicializar los vértices, coordenadas UV, cargar y crear la textura y obtener el identificador de textura en el shader, el método onrender lo usamos para dibujar debemos activar la correspondiente textura y dibujar la figura.

void onstart() override {

	GLuint programID = shader_simple.compile("shaders/simpleA.vertex_shader", "shaders/simpleA.fragment_shader");

	// crear y enlazar un vao
	glGenVertexArrays(1, &vao);
	glBindVertexArray(vao);

	static const float vertex[] =
	{
		-1.0f,  1.0f, 0.0f, 1.5f,
		 1.0f,  1.0f, 0.0f, 1.5f,
		-1.0f, -1.0f, 0.0f, 1.5f,
		 1.0f, -1.0f, 0.0f, 1.5f
	};

	static const float uv[] =
	{
		0.0f, 1.0f,
		1.0f, 1.0f,
		0.0f, 0.0f,
		1.0f, 0.0f
	};

	textureCube = loadBMP("textures/cubo.bmp");

	// generar dos ids para los buffer
	glGenBuffers(2, buffer);

	// enlazar el buffer de vertices
	glBindBuffer(GL_ARRAY_BUFFER, buffer[0]);
	// almacenar datos en el buffer de vertices
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertex), vertex, GL_STATIC_DRAW);

	// describir los datos y activar vPosition (location = 0)
	glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, NULL);
	glEnableVertexAttribArray(0);

	// crear en buffer de coordenadas UV
	glBindBuffer(GL_ARRAY_BUFFER, buffer[1]);
	glBufferData(GL_ARRAY_BUFFER, sizeof(uv), uv, GL_STATIC_DRAW);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, NULL);
	glEnableVertexAttribArray(1);
}

void onrender(double time) override {
	glClear(GL_COLOR_BUFFER_BIT);
	shader_simple.use();
	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

Texturas Múltiples


Podemos aplicar varias texturas a un mismo objeto 3D, glActiveTexture(GL_TEXTURE0); activa el primer texture unit, podemos activar hasta un máximo de 16 texturas (GL_TEXTURE15), luego de esto creamos la textura como lo hicimos anteriormente.

También requerimos modificar el fragment shader para aceptar las demás texturas, para este ejemplo el fragment shader mescla dos texturas usando la función mix, esta función requiere 3 parámetros, la primera textura, la segunda textura y el porcentaje de mezcla, usando 0.4 la función devolverá el 60% de la primera textura mesclado con un 40% de la primera.

in vec2 texCoord;
out vec4 color;

uniform sampler2D myTexture;
uniform sampler2D secondTexture;

void main()
{
      color = mix(texture(myTexture, texCoord), texture(secondTexture, texCoord), 0.4);
}

Al momento de dibujar debemos activar las correspondientes texturas e indicarle al fragment shader cuáles son las texturas con las que trabajaremos, usamos glGetUniformLocation(programID, "myTexture") para obtener el identificador el uniform llamado myTexture y glUniform1i para indicar cual textura corresponde a este uniform.

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureID);
glUniform1i(glGetUniformLocation(programID, "myTexture"), 0);

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, secondTextureID);
glUniform1i(glGetUniformLocation(programID, "secondTexture"), 1);

mezcla de texturas
La función para cargar imágenes BMP está escrita de la manera más simple posible para facilitar su comprensión, no está diseñada para ser eficiente o funcional al 100%.

GitHub: OpenGL Moderno Texturas

Comentarios

Entradas populares de este blog

Conectar SQL Server con Java

Detección de rostros

Instalar OpenCV para Python en Windows