Текстуры в OpenGL

OpenGL and Textures. Triangle with texture. Наложение текстуры на треугольник

В этой статье рассмотрим процесс наложения текстуры на треугольник. Также я продемонстрирую некоторые приемы работы с несколькими текстурами.

Подготовка проекта

Для повторения описываемых здесь действий необходимо скачать проект из статьи о шейдерах. Все изменения будут касаться исходников из той самой статьи.

Что нужно для наложения текстуры?

  1. Сама текстура в формате BMP, для загрузки которой воспользуемся кодом, описанным в этом посте.
  2. Координаты текстуры для каждой из вершин.
  3. Объект OpenGL, соответствующий текстуре.
  4. Fragment shader, отображающий текстурированный объект.

Загрузка текстуры и создание соответствующего объекта

С этой задачей можно справиться, добавив следующий исходник в наш проект:

/*
  Textures source code

*/

#ifndef TEXTURES_C
#define TEXTURES_C

#include <stdio.h>

#define GL_GLEXT_PROTOTYPES

#include <GL/gl.h>
#include <GL/glx.h>
#include <GL/glu.h>
#include <GL/glext.h>
#include <GL/glcorearb.h>

#include "bmp_reader.h"

/* На входе - имя BMP файла, на выходе - идентификатор текстуры в OpenGL */
unsigned int loadBmpTexture(char* fname) {

  BMPInfo* bm_inf = bmp_load(fname); /* Загружаем BMP-файл */
  if(!bm_inf) {
    printf("Error loading bmp: %s\n", bmp_get_err());
    return 0;
  };

  unsigned int tex;
  glGenTextures(1, &amp;tex);             /* Генерируем объект для текстуры */
  glBindTexture(GL_TEXTURE_2D, tex);  /* Делаем его активным */

  /*  Если глубина цвета не 32 бита, то используем модеть RGB,
      в противном случае - RGBA */
  if(bm_inf->dib.bpp != 32)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, bm_inf->dib.width,
      bm_inf->dib.height, 0, GL_RGB,
      GL_UNSIGNED_BYTE, bm_inf->data);
  else
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bm_inf->dib.width,
      bm_inf->dib.height, 0, GL_RGBA,
      GL_UNSIGNED_BYTE, bm_inf->data);

  /* Генерируем уменьшеные копии изображений (для удаленных от камеры объектов) */
  glGenerateMipmap(GL_TEXTURE_2D);
  bmp_free(bm_inf); /*  Как только объект текстуры сформирован,
                        исходные данные можно удалить */
  return tex;
};

#endif

К этому исходнику следует добавить заголовочный файл, содержащий описание функции loadBmpTexture.

/*
  Textures header

*/

#ifndef TEXTURES_H
#define TEXTURES_H

unsigned int loadBmpTexture(char* fname);

#endif

Включив этот заголвочный файл в main.c, мы сможем загрузить необходимые текстуры.

Координаты текстуры

Теперь поговорим о втором пункте из списка выше. Дело в том, что «натянуть» изображение на треугольник можно по разному. Для определенности следует установить соответствие между точками на поверхности текстуры и вершинами треугольника.

Координаты на поверхности текстуры определены следующим образом: левый верхний угол — точка (0, 0), правый верхний угол — (1, 0), левый нижний угол — (0, 1), правый нижний угол — (1, 1). Такое распределение координат сохранится при любом соотношении сторон изображения.

OpenGL Texture coordinate system

В нашем случае потребуется средняя часть исходной текстуры.

OpenGL Texture coordinates for single trianle

Фрагмен, обозначенный серым цветом, попадет на треугольник. Остальная часть изображения не будет использована. Красным здесь выделены координаты текстуры для соответствующих вершин треугольника.

Добавим эти координаты к уже имеющимся координатам вершин в основном исходнике:

	float verts[] = {	0.0, 0.9, 0.0, 0.5, 0.0,
										0.9, -0.9, 0.0, 1.0, 1.0,
										-0.9, -0.9, 0.0, 0.0, 1.0};

Дополняем VAO

В массиве с информацией о вершинах появился новый атрибут — координаты текстуры. Следовательно, нам необходимо добавить описание этого атрибута в функцию формирования VAO. Откроем исходник vertexes.c и добавим в функцию CreateVAO следующее содержимое:

  /* Атрибут 0 соответствует координатам вершины */
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
  glEnableVertexAttribArray(0); /* Активируем атрибут 0 */
  /* Атрибут 1 соответствует координатам текстуры */
  glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3*sizeof(float)));
  glEnableVertexAttribArray(1);

Атрибут 0 уже был определен. Но так как добавился ещё один атрибут (два компонента типа float), общий объем данных для каждой вершины увеличился на 2 * размер типа float. Таким образом, параметр stride нулевого и первого атрибутов стал равен 5 * sizeof(float).

Как видно из определения массива verts, координаты текстуры распологаются сразу после координат вершины. Следовательно, смещение для атрибута 1 будет равно 3*sizeof(float). То есть, чтобы добраться до координат текстуры, нужно «перепрыгнуть» через координаты вершины (3 компонента типа float).

Шейдеры

Массив с данными вершин определен, функция для загрузки текстуры из файла создана. Теперь сформируем шейдеры для наложения текстуры. Напомню, что исходники шейдеров хранятся в файлах с именами vertex_shader.glsl и fragment_shader.glsl. Перепишем содержимое этих файлов следующим образом:

vertex_shader.glsl

#version 300 es

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 tCoords;
out vec3 pos;
out vec2 tcoords;

void main() {
  pos = aPos;
  tcoords = tCoords;
  gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

fragment_shader.glsl

#version 300 es
precision mediump float;
out vec4 FragColor;
in vec3 pos;
in vec2 tcoords;

uniform sampler2D tex;

void main() {
  FragColor = texture(tex, tcoords);
}

Начнем с vertex-шейдера. В начале кода мы определяем две переменные, соответствующие атрибутам, определенным в VAO. Это координаты вершины треугольника и координаты текстуры соответственно (aPos и tCoords). Здесь параметр location как раз и определяет — к какому атрибуту относится переменная.

Среди выходных переменных шейдера мы определили переменную tcoords для того, чтобы передать координаты текстуры в fragment-шейдер. Эта переменная во fragment-шейдере определена как входная (ключевое слово in).

А вот сама текстура передается через переменную tex типа sampler2D. Чтобы отправить данные изображения во FragColor, используем функцию texture.

Ключевое слово uniform говоирт о том, что переменная является глобальной. Значения таких переменных могут быть заданы с помощью определенных функций OpenGL, о чем будет сказано позже.

Дополняем основной исходник (main.c)

	/* Создаем шейдерную программу */
	unsigned int shaderProgram = CreateShaderProgram(argv[1], argv[2]);

	/* Определяем координаты вершин треугольника
		с соответствующими координатами текстуры */
	float verts[] = {	0.0, 0.9, 0.0, 0.5, 0.0,
										0.9, -0.9, 0.0, 1.0, 1.0,
										-0.9, -0.9, 0.0, 0.0, 1.0};

	/* Создаем объект, описывающий атрибуты вершин */
	unsigned int VAO = CreateVAO(verts, sizeof(verts));
	/* Загружаем текстуру из указанного файла */
	unsigned int my_tex = loadBmpTexture(argv[3]);

В основном цикле для рисования текстурированного треугольника используем следующий набор процедур:

		glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		glUseProgram(shaderProgram); /* Активируем шейдерную программу */
		glBindTexture(GL_TEXTURE_2D, my_tex); /* Делаем текстуру активной */
		glBindVertexArray(VAO); /* Активируем массив с данными о вершинах */
		glDrawArrays(GL_TRIANGLES, 0, 3); /* Отображаем треугольник */

В коде нашего проекта не было упоминаний переменной tex из fragment-шейдера. Тем не менее, система сама поместит данные активного обекта текстуры в эту переменную.

Если текстур несколько

В системе OpenGL существует механизм, позволяющий использовать несколько текстур одновременно. Это может понадобиться при формировании сложного материала. Например, при создании эффекта освещения рельефной поверхности. В таком случае понадобится, помимо основной текстуры, карта нормалей к поверхности (normal map).

Предположим, что нам нужно использовать две текстуры в одном материале. Тогда нужно выполнить следующее:

/* Загрузить текстуры, используя соответствующие имена файлов */
unsigned int my_tex0 = loadBmpTexture("texture0.bmp");
unsigned int my_tex1 = loadBmpTexture("texture1.bmp");

/* Так как переменных типа sampler2D в fragment шейдере будет две, необходимо установить соответствие между ними и индексами текстур */

/* Делаем шейдерную программу активной */
glUseProgram(shaderProgram);

/* Устанавливаем соответствие переменной tex0 нулевому индексу */
glUniform1i(glGetUniformLocation(shaderProgram, "tex0"), 0);

/* Устанавливаем соответствие переменной tex1 индексу 1 */
glUniform1i(glGetUniformLocation(shaderProgram, "tex1"), 1);

............
/* В основном цикле приложения */
/* Назначить индекс активной текстуры */
glActiveTexture(GL_TEXTURE0);
/* Прикрепить текстуру к данному индексу */
glBindTexture(GL_TEXTURE_2D, my_tex0);
/* Проделать то же самое для второй текстуры, но с индексом 1 */
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, my_tex1);

/* Далее выбираем объект VAO и рисуем треугольник */
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

Исходя из текста выше, глобальные переменные шейдеров для текстур — это tex0 и tex1. Следовательно, исходный код fragment-шейдера будет выглядеть так:

#version 300 es
precision mediump float;
out vec4 FragColor;
in vec3 pos;
in vec2 tcoords;

uniform sampler2D tex0;
uniform sampler2D tex1;

void main() {
  FragColor = mix(texture(tex0,tcoords),texture(tex1,tcoords),0.5);
}

Здесь функция mix смешивает текстуры в пропорции 1:1, о чем говорит параметр 0.5.

Стоит сказать пару слов о glActiveTexture. В случае использования glBindTexture несколько раз подряд, каждый следующий вызов будет деактивировать предыдущую текстуру и делать активной текущую. В результате этого активная текстура будет всегда одна. Чтобы обойти этот момент и сделать несколько текстур активными одновременно, как раз и существует glActiveTexture. Эта процедура усатанавливает номер «слота» для активируемой текстуры.

Таких «слотов» существует как минимум 16: от GL_TEXTURE0 до GL_TEXTURE15. Это не значит, что в формировании сцены могут участвовать только 16 текстур. Это значит, что в одном материале могут сочитаться 16 текстур.

9

Добавить комментарий

Ваш e-mail не будет опубликован.