Урок 5: Текстурированный куб
Добро пожаловать на наш пятый урок. В этом уроке вы узнаете:
- Что такое UV-координаты
- Как самостоятельно загружать текстуры
- Как использовать их в OpenGL
- Что такое фильтрация и мип-маппинг и как их использовать
- Как загружать текстуры с помощью GLFW
- Что такое Alpha-канал
UV-координаты
Когда вы текстурируете какой-то объект, то вам необходимо как-то сообщить OpenGL, какая часть изображения прикрепляется к каждому треугольнику. Именно для этого и используются UV-координаты
Каждая вершина помимо позиции имеет несколько дополнительных полей, а также U и V. Эти координаты используются применительно к текстуре, как показано на рисунке:
Обратите внимание, как текстура искажается на треугольнике.
Загрузка Bitmap-изображений
Знание формата файлов BMP не является критичным, так как многие библиотеки могут сделать загрузку за вас. Однако, чтобы лучше понимать то, что происходит в таких библиотеках мы разберем ручную загрузку.
Объявляем функцию для загрузки изображений:
GLuint loadBMP_custom(const char * imagepath);
Вызываться она будет так:
GLuint image = loadBMP_custom("./my_texture.bmp");
Теперь перейдем непосредственно к чтению файла.
Для начала, нам необходимы некоторые данные. Эти переменные будут установлены когда мы будем читать файл:
// Данные, прочитанные из заголовка BMP-файла unsigned char header[54]; // Каждый BMP-файл начинается с заголовка, длиной в 54 байта unsigned int dataPos; // Смещение данных в файле (позиция данных) unsigned int width, height; unsigned int imageSize; // Размер изображения = Ширина * Высота * 3 // RGB-данные, полученные из файла unsigned char * data;
FILE * file = fopen(imagepath,"rb"); if (!file)
Первым, в BMP-файлах идет заголовок, размером в 54 байта. Он содержит информацию о том, что файл действительно является файлом BMP, размер изображение, количество бит на пиксель и т. п., поэтому читаем его:
if ( fread(header, 1, 54, file) != 54 ) < // Если мы прочитали меньше 54 байт, значит возникла проблема printf("Некорректный BMP-файлn"); return false; >
Заголовок всегда начинается с букв BM. Вы можете открыть файл в HEX-редакторе и убедиться в этом самостоятельно, а можете посмотреть на наш скриншот:
Итак, мы проверяем первые два байта и если они не являются буквами “BM”, то файл не является BMP-файлом или испорчен:
if ( header[0]!='B' || header[1]!='M' )
Теперь мы читаем размер изображения, смещение данных изображения в файле и т. п.:
// Читаем необходимые данные dataPos = *(int*)&(header[0x0A]); // Смещение данных изображения в файле imageSize = *(int*)&(header[0x22]); // Размер изображения в байтах width = *(int*)&(header[0x12]); // Ширина height = *(int*)&(header[0x16]); // Высота
Проверим и исправим полученные значения:
// Некоторые BMP-файлы имеют нулевые поля imageSize и dataPos, поэтому исправим их if (imageSize==0) imageSize=width*height*3; // Ширину * Высоту * 3, где 3 - 3 компоненты цвета (RGB) if (dataPos==0) dataPos=54; // В таком случае, данные будут следовать сразу за заголовком
Теперь, так как мы знаем размер изображения, то можем выделить область памяти, в которую поместим данные:
// Создаем буфер data = new unsigned char [imageSize]; // Считываем данные из файла в буфер fread(data,1,imageSize,file); // Закрываем файл, так как больше он нам не нужен fclose(file);
Следует отметить, что приведенный код может быть использован только для загрузки 24-битных изображений (т. е. где на каждый пиксель изображения отводится 3 байта). С другими форматами BMP-файла вам следует познакомиться самостоятельно.
Мы вплотную подошли к части, касающейся OpenGL. Создание текстур очень похоже на создание вершинных буферов:
- Создайте текстуру
- Привяжите ее
- Заполните
- Сконфигурируйте
GL_RGB в glTextImage2D указывает на то, что мы работает с 3х компонентным цветом. А GL_BGR указывает на то, как данные представлены в памяти. На самом деле в BMP-файлах цветовые данные хранятся не в RGB, а в BGR (если быть точным, то это связано с тем, как хранятся числа в памяти), поэтому необходимо сообщить об этом OpenGL:
// Создадим одну текстуру OpenGL GLuint textureID; glGenTextures(1, &textureID); // Сделаем созданную текстуру текущий, таким образом все следующие функции будут работать именно с этой текстурой glBindTexture(GL_TEXTURE_2D, textureID); // Передадим изображение OpenGL glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, width, height, 0, GL_BGR, GL_UNSIGNED_BYTE, data); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
Последние две строки мы поясним позднее, а пока в части C++ мы должны использовать нашу функцию для загрузки текстуры:
GLuint Texture = loadBMP_custom("uvtemplate.bmp");
**Очень важное замечание: **используйте текстуры с шириной и высотой степени двойки! То есть:
- Хорошие: 128128, 256256, 10241024, 2*2…
- Плохие: 127128, 35, …
- Приемлемые: 128*256
Использование текстуры в OpenGL
Что же, давайте посмотрим на наш Фрагментный шейдер:
#version 330 core // Интерполированные значения из вершинного шейдера in vec2 UV; // Выходные данные out vec3 color; // Значения, которые остаются неизменными для объекта. uniform sampler2D myTextureSampler; void main() < // Выходной цвет = цвету текстуры в указанных UV-координатах color = texture( myTextureSampler, UV ).rgb; >
- Фрагментному шейдеру требуются UV-координаты. Это понятно.
- Также, ему необходим “sampler2D”, чтобы знать, с какой текстурой работать (вы можете получить доступ к нескольким текстурам в одном шейдере т. н. мультитекстурирование)
- И наконец, доступ к текстуре завершается вызовом texture(), который возвращает vec4 (R, G, B, A). A-компоненту мы разберем немного позднее.
Вершинный шейдер также прост. Все, что мы делаем — это передаем полученные UV-координаты в фрагментный шейдер:
#version 330 core // Входные данные вершин, различные для всех запусков этого шейдера layout(location = 0) in vec3 vertexPosition_modelspace; layout(location = 1) in vec2 vertexUV; // Выходные данные, которые будут интерполированы для каждого фрагмента out vec2 UV; // Значения, которые останутся неизменными для всего объекта uniform mat4 MVP; void main() < // Выходная позиция вершины gl_Position = MVP * vec4(vertexPosition_modelspace,1); // UV-координаты вершины. UV = vertexUV; >
Помните “layout(location = 1) in vec3 vertexColor” из Урока 4? Здесь мы делаем абсолютно тоже самое, только вместо передачи буфера с цветом каждой вершины мы будем передавать буфер с UV-координатами каждой вершины:
// Две UV-координаты для каждой вершины. Они были созданы с помощью Blender. Мы коротко расскажем о том, как сделать это самостоятельно. static const GLfloat g_uv_buffer_data[] = < 0.000059f, 1.0f-0.000004f, 0.000103f, 1.0f-0.336048f, 0.335973f, 1.0f-0.335903f, 1.000023f, 1.0f-0.000013f, 0.667979f, 1.0f-0.335851f, 0.999958f, 1.0f-0.336064f, 0.667979f, 1.0f-0.335851f, 0.336024f, 1.0f-0.671877f, 0.667969f, 1.0f-0.671889f, 1.000023f, 1.0f-0.000013f, 0.668104f, 1.0f-0.000013f, 0.667979f, 1.0f-0.335851f, 0.000059f, 1.0f-0.000004f, 0.335973f, 1.0f-0.335903f, 0.336098f, 1.0f-0.000071f, 0.667979f, 1.0f-0.335851f, 0.335973f, 1.0f-0.335903f, 0.336024f, 1.0f-0.671877f, 1.000004f, 1.0f-0.671847f, 0.999958f, 1.0f-0.336064f, 0.667979f, 1.0f-0.335851f, 0.668104f, 1.0f-0.000013f, 0.335973f, 1.0f-0.335903f, 0.667979f, 1.0f-0.335851f, 0.335973f, 1.0f-0.335903f, 0.668104f, 1.0f-0.000013f, 0.336098f, 1.0f-0.000071f, 0.000103f, 1.0f-0.336048f, 0.000004f, 1.0f-0.671870f, 0.336024f, 1.0f-0.671877f, 0.000103f, 1.0f-0.336048f, 0.336024f, 1.0f-0.671877f, 0.335973f, 1.0f-0.335903f, 0.667969f, 1.0f-0.671889f, 1.000004f, 1.0f-0.671847f, 0.667979f, 1.0f-0.335851f >;
Указанные UV-координаты относятся к такой модели:
Остальное очевидно. Мы создаем буфер, привязываем его, заполняем, настраиваем и выводим Буфер Вершин как обычно. Только будьте осторожны, так как в glVertexAttribPointer для буфера текстурных координат второй параметр (размер) будет не 3, а 2.
И вот такой результат мы получим:
в увеличенном варианте:
Фильтрация и мип-маппинг.
Как вы можете видеть на скриншоте выше, качество текстуры не очень хорошее. Это потому, что в нашей процедуре загрузки BMP-изображения (loadBMP_custom) мы указали:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
Это означает, что в нашем фрагментном шейдере, texture() возвращает строго тексель, который находится по указанным текстурным координатам:
Есть несколько решений, которые позволят улучшить ситуацию.
При помощи линейной фильтрации texture() будет смешивать цвета находящихся рядом текселей в зависимости от дистанции до их центра, что позволит предотвратить резкие границы, которые вы видели выше:
Это будет выглядить значительно лучше и используется часто, но если вы хотите очень высокого качества, то вам понадобится анизотропная фильтрация, которая работает несколько медленнее.
Аппроксимирует часть изображения, которая действительно видна через фрагмент. К примеру, если указанная текстура просматривается сбоку и немного повернута, то анизотропная фильтрация будет вычислять цвет, который находится в синем прямоугольнике, с помощью фиксированного количество сэмплов (Уровень анизотропии) вдоль его направления:
И линейная, и анизотропная фильтрация имеют недостаток. Если текстура просматривается с большого расстояния, то смешивать 4 текселя будет недостаточно. То есть, если ваша 3D модель находится так далеко, что занимает на экране всего 1 фрагмент, то фильный цвет фрагмента будет являться средним всех текселей текстуры. Естественно, это не реализовано из-за соображений производительности. Для этой цели существует так называемый мип-маппинг:
- При инициализации вы уменьшаете масштаб текстуры до тех пор, пока не получите изображение 1х1 (которое по сути будет являться средним значением всех текселей текстуры)
- Когда вы выводите объект, то вы выбираете тот мип-мап, который наиболее приемлем в данной ситуации.
- Вы применяете к этому мип-мапу фильтрацию
- А для большего качества вы можете использовать 2 мип-мапа и смешать результат.
К счастью для нас, все это делается очень просто с помощью OpenGL:
// Когда изображение увеличивается, то мы используем обычную линейную фильтрацию glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // Когда изображение уменьшается, то мы используем линейной смешивание 2х мипмапов, к которым также применяется линейная фильтрация glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); // И генерируем мипмап glGenerateMipmap(GL_TEXTURE_2D);
Загрузка текстур с помощью GLFW
Наша процедура loadBMP_custom великолепна, так как мы сделали ее сами, но использование специальных библиотек может быть предпочтительнее (в конечном итоге мы в своей процедуре многое не учли). GLFW может сделать это лучше (но только для TGA-файлов):
GLuint loadTGA_glfw(const char * imagepath) < // Создаем одну OpenGL текстуру GLuint textureID; glGenTextures(1, &textureID); // "Привязываем" только что созданную текстуру и таким образом все последующие операции будут производиться с ней glBindTexture(GL_TEXTURE_2D, textureID); // Читаем файл и вызываем glTexImage2D с необходимыми параметрами glfwLoadTexture2D(imagepath, 0); // Трилинейная фильтрация. 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_MIPMAP_LINEAR); glGenerateMipmap(GL_TEXTURE_2D); // Возвращаем идентификатор текстуры который мы создали return textureID; >
Сжатые текстуры
На этом шаге вы наверное хотите узнать, как же все-таки загружать JPEG файлы вместо TGA?
Короткий ответ: даже не думайте об этом. Есть идея получше.
##Создание сжатых текстур
- Скачайте The Compressonator, утилита от ATI
- Загрузите в нее текстуру, размер которой является степенью двойки
- Сожмите ее в DXT1, DXT3 или в DXT5 (о разнице между форматами можете почитать на Wikipedia)
- Создайте мипмапы, чтобы не создавать их во время выполнения программы.
- Экспортируйте это как .DDS файл
После этих шагов вы имеете сжатое изображение, которое прямо совместимо с GPU. И когда вы вызовите texture() в шейдере, то текстура будет распакована на лету. Это может показаться более медленным, однако это требует гораздо меньше памяти, а значит пересылаемых данных будет меньше. Пересылка данных всегда будет дорогой операцией, в то время как декомпрессия является практически бесплатной. Как правило, использование сжатия текстур повышает быстродействие на 20%.
##Использование сжатой текстуры
Теперь перейдем непосредственно к загрузке нашей сжатой текстуры. Процедура будет очень похожа на загрузку BMP, с тем исключением, что заголовок файла будет организован немного иначе:
GLuint loadDDS(const char * imagepath) < unsigned char header[124]; FILE *fp; /* пробуем открыть файл */ fp = fopen(imagepath, "rb"); if (fp == NULL) return 0; /* проверим тип файла */ char filecode[4]; fread(filecode, 1, 4, fp); if (strncmp(filecode, "DDS ", 4) != 0) < fclose(fp); return 0; >/* читаем заголовок */ fread(&header, 124, 1, fp); unsigned int height = *(unsigned int*)&(header[8 ]); unsigned int width = *(unsigned int*)&(header[12]); unsigned int linearSize = *(unsigned int*)&(header[16]); unsigned int mipMapCount = *(unsigned int*)&(header[24]); unsigned int fourCC = *(unsigned int*)&(header[80]);
После заголовку идут данные, в которые входят все уровни мип-мап. К слову, мы можем прочитать их все сразу:
unsigned char * buffer; unsigned int bufsize; /* вычисляем размер буфера */ bufsize = mipMapCount > 1 ? linearSize * 2 : linearSize; buffer = (unsigned char*)malloc(bufsize * sizeof(unsigned char)); fread(buffer, 1, bufsize, fp); /* закрываем файл */ fclose(fp);
Сделано. Так как мы можем использовать 3 разных формата (DXT1, DXT3, DXT5), то необходимо в зависимости от флага “fourCC”, сказать OpenGL о формате данных.
unsigned int components = (fourCC == FOURCC_DXT1) ? 3 : 4; unsigned int format; switch(fourCC)
Создание текстуры выполняется как обычно:
// Создаем одну OpenGL текстуру GLuint textureID; glGenTextures(1, &textureID); // "Привязываем" текстуру. glBindTexture(GL_TEXTURE_2D, textureID);
Следующим шагом мы загружаем мип-мапы:
unsigned int blockSize = (format == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT) ? 8 : 16; unsigned int offset = 0; /* загрузка мип-мапов */ for (unsigned int level = 0; level < mipMapCount && (width || height); ++level) < unsigned int size = ((width+3)/4)*((height+3)/4)*blockSize; glCompressedTexImage2D(GL_TEXTURE_2D, level, format, width, height, 0, size, buffer + offset); offset += size; width /= 2; height /= 2; >free(buffer); return textureID;
DXT компрессия пришла к нам из DirectX, где координатная текстура V является инвертированной по сравнению с OpenGL. Поэтому, если вы используете сжатые текстуры, то вам необходимо использовать (coord.u, 1.0 — coord.v), чтобы исправить тексель. Вы можете выполнять это как при экспорте текстуры, так и в загрузчике или в шейдере.
Заключение
В данном уроке вы узнали как создавать, загружать и использовать текстуры в OpenGL.
Стоит отметить, что в своих проектах мы настоятельно рекомендуем вам использовать только сжатые текстуры, так как они занимают меньше места, быстрее загружаются и используются. Для этих целей можете также использовать The Compressonator.
Упражнения
- В исходный код к урокам включен загрузчик DDS, но без исправления текстурных координат. Модифицируйте код так, чтобы корректно выводить куб.
- Поэкспериментируйте с разными DDS форматами. Дают ли они разный результат или разную степень сжатия?
- Попробуйте не создавать мип-мапы в The Compressonator. Каков результат? Создайте 3 пути решения этих проблем.
Полезные ссылки
- Using texture compression in OpenGL , Sébastien Domine, NVIDIA
learnopengl. Урок 1.6 — Текстуры
В прошлом уроке мы научились раскрашивать наши объекты в разные цвета. Но для того, чтобы добиться некого реализма нам потребуется очень много цветов. В прошлый раз, мы раскрашивали вершины треугольника, если мы пойдем тем же путем, то нам понадобится слишком большое количество вершин для вывода картинки. Заинтересовавшихся, прошу под кат.
Содержание
Часть 2. Базовое освещение
Часть 3. Загрузка 3D-моделей
- Библиотека Assimp
- Класс полигональной сетки Mesh
- Класс 3D-модели
Часть 4. Продвинутые возможности OpenGL
- Тест глубины
- Тест трафарета
- Смешивание цветов
- Отсечение граней
- Кадровый буфер
- Кубические карты
- Продвинутая работа с данными
- Продвинутый GLSL
- Геометричечкий шейдер
- Инстансинг
- Сглаживание
Часть 5. Продвинутое освещение
- Продвинутое освещение. Модель Блинна-Фонга.
- Гамма-коррекция
- Карты теней
- Всенаправленные карты теней
Программисты и художники предпочитают использовать текстуры. Текстура — это 2D изображение (1D и 3D текстура также существуют), используемое для добавления деталей объекту; считайте, что текстура — это кусок бумаги с картинкой кирпича (к примеру), который наклеен на ваш дом и кажется, что ваш дом сделан из кирпича.
Помимо картинок, текстуры могут хранить большие наборы данных, отправляемых в шейдеры, но мы оставим этот вопрос для другого урока. Ниже вы можете видеть текстуру кирпичной стены прилепленной на треугольник из прошлого урока.
Для того, чтобы привязать текстуру к треугольнику мы должны сообщить каждой вершине треугольника, какой части текстуры принадлежит эта вершина. Каждая вершина, соответственно должна иметь текстурные координаты, ассоциированные с частью текстуры.
Текстурные координаты находятся в промежутке между 0 и 1 по x и y оси (мы же используем 2D текстуры). Получение цвета текстуры с помощью текстурных координат называется отбором (sampling). Текстурные координаты начинаются в (0, 0) в нижнем левом углу текстуры и заканчиваются на (1, 1) в верхнем правом углу. Изображение ниже демонстрирует как мы накладывали текстурные координаты на треугольник:
Мы указали 3 текстурные координаты для треугольника. Мы хотим, чтобы нижний левый угол треугольника соотносился с нижним левым углом текстуры, поэтому мы передаем (0, 0) нижней левой вершине треугольника. Соответственно в нижнюю правую вершину передаем (1, 0). Верхняя вершина треугольника должна соотноситься с центральной частью верхней стороной текстуры, поэтому мы передаем в верхнюю вершину (0.5, 1.0) в качестве текстурной координаты.
В результате текстурные координаты для треугольника должны выглядеть как-то так:
GLfloat texCoords[] = < 0.0f, 0.0f, // Нижний левый угол 1.0f, 0.0f, // Нижний правый угол 0.5f, 1.0f // Верхняя центральная сторона >;
Сэмплинг текстуры может быть выполнен различными методами. Наша работа — сообщить OpenGL как он должен проводить сэмплинг.
Texture Wrapping
Текстурные координаты, зачастую, находятся в промежутке между (0,0) и (1,1), но что произойдет, если текстурные координаты выйдут за этот промежуток? Поведение OpenGL по-умолчанию — повторять изображение (фактически, просто игнорируется целая часть числа с плавающей точкой), но также есть и другие опции:
- GL_REPEAT: Стандартное поведение для текстур. Повсторяет текстуру.
- GL_MIRRORED_REPEAT: Похоже на _GLREPEAT за исключением того, что в этом режиме изображение отражается.
- GL_CLAMP_TP_EDGE: Привязывает координаты между 0 и 1. В результате выходящие за пределы координаты будут привязаны к границе текстуры.
- GL_CLAMP_TO_BORDER: Координаты, выходящие за пределы диапазона будут давать установленный пользователем цвет границы.
Каждая их этих опций по разному отображается при использовании текстурных координат, выходящих за пределы промежутка. Изображение ниже отлично демонстрирует различия:
Каждую из вышепредставленных опций можно установить на оси (s, t (и r если вы используете 3D текстуры), эквивалентны x, y и z) с помощью функций **glTextParameter***:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
Первый аргумент определяет цель к которой привязана наша текстура, мы работаем с 2D текстурой, поэтому наше значение будет GL_TEXTURE_2D. Второе значение требуется для того, чтобы сообщить OpenGL какой конкретно параметр мы хотим установить. Мы хотим настроить опцию WRAP и указать ее значение для осей S и T. В последнем аргументе передается выбранный метод wrapping. В данном случае мы используем GL_MIRRORED_REPEAT.
Если бы мы выбрали GL_CLAMP_TO_BORDER, то нам бы еще пришлось указать цвет границ. Делается это fv альтернативой glTextParameter с передачей в нее GL_TEXTURE_BORDER_COLOR в качестве опции и массива из чисел с плавающей точкой в качестве цветового значения.
float borderColor[] = < 1.0f, 1.0f, 0.0f, 1.0f >; glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
Фильтрование текстур
Текстурные координаты не зависят от разрешения, но при этом могут принимать любые значения с плавающей точкой, поэтому OpenGL требуется понять какой пиксель текстуры (также называемого текселем) ему требуется наложить. Эта проблема становится наиболее острой если требуется наложить текстуру низкого разрешения на большой объект. Возможно вы уже догадались, что в OpenGL есть опция для фильтрования текстур. Есть несколько доступных опций, но мы обсудим только наиболее важные: GL_NEAREST и GL_LINEAR.
GL_NEAREST (также называемый фильтр ближайшего соседа) — стандартный метод фильтрования в OpenGL. Пока он установлен OpenGL будет выбирать пиксель, который находится ближе всего к текстурной координате. Ниже вы можете видеть 4 пикселя и крест, показывающий текстурную координату. Поскольку центр верхнего левого тексель ближе всего к текстурной координате, то он и выбирается в качестве цвета сэмпла.
GL_LINEAR (также называемый (би)линейной фильтрацией). Принимает интерполированное значение от ближайших к текстурной координате текселей. Чем ближе тексель к текстурной координате, тем больше множитель цвета этого текселя.
Ниже вы можете видеть пример смешивания цветов соседних пикселей:
Но какой же все таки визуальный эффект от выбранного эффекта фильтрования? Давайте посмотрим как эти методы отработают с текстурой в маленьком разрешении на большом объекте (текстура была увеличена для того, чтобы было видно отдельные тексели):
Mipmaps
Представьте, что у вас есть большая комната с тысячами объектов, к каждому из которых привязана текстура. Часть объектов ближе к наблюдателю, часть объектов дальше от наблюдателя и каждому объекту привязана текстура с высоким разрешением. Когда объект находится далеко от наблюдателя, требуется обработать только несколько фрагментов. У OpenGL есть сложности с получением правильного цвета для фрагмента с текстуры высокого разрешения, когда приходится учитывать большое количество пикселей текстуры. Такое поведение будет генерировать артефакты на маленьких объектах, не говоря уже о чрезмерной трате памяти связанной с использованием текстур высокого разрешения на маленьких объектах.
Для решения этой проблемы OpenGL использует технологию, называемую мипмапами (mipmaps), которая предусматривает набор изображений-текстур где каждая последующая текстура вдвое меньше прошлой. Идея, которая лежит в основе мипмапов довольно проста: после определенного расстояния от наблюдателя, OpenGL будет использовать другую мипмап текстуру, которая будет лучше выглядеть на текущем расстоянии. Чем дальше от наблюдателя находится объект тем меньше будет использоваться текстура, поскольку пользователю сложнее будет заметить разницу между разрешениями текстур.Также мипмапы имеют приятное свойство увеличивать производительность, что никогда не бывает лишним. Давайте посмотрим на пример мипмапы поближе:
Создание набора мипмап текстур для каждого изображения довольно муторно, но к счастью OpenGL умеет генерировать их с помощью вызова glGenerateMipmaps после создания текстуры. Скоро мы увидим пример.
Во время переключения между уровнями мипмапов в процессе отрисовки OpenGL может отображать некоторые артефакты, вроде острых краев между двумя уровнями. Также как возможно использование фильтрации на текстурах, также можно использование фильтрации на различных уровнях мипмапов с помощью NEAREST и LINEAR фильтрации для переключения между уровнями. Для указания способа фильтрации между уровнями мипмапами мы можем заменить стандартные методы одной из следующих четырех настроек:
- _GL_NEARESET_MIPMAPNEAREST: Выбирает ближайший мипмап, соотносящийся с размером пикселя и также используется интерполяция ближайшего соседа для сэмплинга текстур.
- _GL_LINEAR_MIPMAPNEAREST: Выбирает ближайший мипмап и сэмплирует его методом линейной интерполяции.
- _GL_NEAREST_MIPMAPLINEAR: Линейная интерполяция между двумя ближайшими мипмапами и сэмплирование текстур с помощью линейной интерполяции.
Также как и с фильтрацией текстур мы можем установить метод фильтрации с помощью функции glTexParameteri
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Частая ошибка — это установка метода фильтрации мипмапов в качестве увеличивающего фильтра. Это не даст никакого эффекта, поскольку мипмапы в основном используют при уменьшении текстуры. Увеличение текстуры не использует мипмапы, поэтому при передаче ему опции фильтрации мипмапа сгенерирует ошибку GL_INVALID_ENUM.
Загрузка и создание текстур
Перед тем как начать использовать наши текстуры нам требуется их загрузить в наше приложение. Текстурные изображения могут храниться в безграничном количестве форматов, в каждом из которых своя структура и упорядоченность данных, так как же мы передадим наше изображение в приложение? Одним из решений является использование удобного нам формата, к примеру .PNG и написать собственную систему загрузки изображений в большой массив байт. Хоть написание собственного загрузчика изображений не представляет собой неподъемную работу, все-таки это довольно утомительно, тем более если вы захотите использовать много форматов файлов.
Другим решением является использование готовой библиотеки для загрузки изображений, которая бы поддерживала множество различных популярных форматов и делала много тяжелой работы за нас. К примеру SOIL.
SOIL
SOIL расшифровывается как Simple OpenGL Image Library, поддерживает большинство популярных форматов изображений, легка в использовании и может быть скачана отсюда. Также как и большинство других библиотек вам придется сгенерировать файл .lib самостоятельно. Вы можете использовать один из их проектов, располагающихся в папке /projects (не волнуйтесь, если версия их проектов будет ниже версии вашей VS. Просто сконвертируйте их в новую версию, это должно работать в большинстве случаев) для создания на его основе собственного. Также добавьте содержимое папки src в свою папку include. Также не забудьте добавить SOIL.lib в настройки своего линковщика и добавить #include в начале вашего кода.
Для текущей текстурной секции мы будем использовать изображение деревянного контейнера. Для загрузки изображения через SOIL мы используем функцию SOIL_load_image:
int width, height; unsigned char* image = SOIL_load_image("container.jpg", &width, &height, 0, SOIL_LOAD_RGB);
Первый аргумент функции — это местоположение файла изображения. Второй и третий аргументы — это указатели на int в которые будут помещены размеры изображения: ширина и высота. Они нам понадобятся для генерации текстуры. Четвертый аргумент — это количество каналов изображения, но мы оставим там просто 0. Последний аргумент сообщает SOIL как ему загружать изображение: нам нужна только RGB информация изображения. Результат будет храниться в большом массиве байт.
Генерация текстуры
Также как и на любой другой объект в OpenGL, на текстуры ссылаются идентификаторы. Давайте создадим один:
GLuint texture; glGenTextures(1, &texture);
Функция glGenTextures принимает в качестве первого аргумента количество текстур для генерации, а в качестве второго аргумента — массив GLuint в котором будут храниться идентификаторы этих текстур (в нашем случае это один GLuint). Также как любой другой объект мы привяжем его для того, чтобы функции, использующие текстуры, знали какую текстуру использовать.
glBindTexture(GL_TEXTURE_2D, texture);
После привязки текстуры мы можем начать генерировать данные текстуры используя предварительно загруженное изображение. Текстуры генерируются с помощью glTexImage2D:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image); glGenerateMipmap(GL_TEXTURE_2D);
У этой функции довольно много аргументов, поэтому давайте по порядку:
- Первый аргумент описывает текстурную цель. Установив значение GL_TEXTURE_2D мы сообщили функции, что наша текстура привязана к этой цели (чтобы другие цели GL_TEXTURE_1D и GL_TEXTURE_3D не будут задействованы).
- Второй аргумент описывает уровень мипмапа для которого мы хотим сгенерировать текстуру, если вдруг мы хотим самостоятельно сгенерировать мипмапы. Поскольку мы оставим генерацию мипмапов на OpenGL мы передадим 0.
- Третий аргумент сообщает OpenGL в каком формате мы хотим хранить текстуру. Поскольку наше изображение имеет только RGB значения то и в текстуры мы также будем хранить только RGB значения.
- Четвертый и пятый аргументы задают ширину и высоту результирующей текстуры. Мы получили эти значения ранее во время загрузки изображения.
- Шестой аргумент всегда должен быть 0. (Аргумент устарел).
- Седьмой и восьмой аргументы описывают формат и тип данных исходного изображения. Мы загружали RGB значения и хранили их в байтах (char) так что мы передаем эти значения.
- Последний аргумент — это сами данные изображения.
После вызова glTexImage2D текущая привязанная текстура будет иметь привязанное к ней изображение. Правда текстура будет иметь только базовое изображение и если мы захотим использовать мипмапы, то нам придется таким же образом задавать изображение просто инкрементируя значение уровня мипмапов. Ну или мы можем просто вызвать glGenerateMipmap после генерации текстуры. Эта функция автоматически сгенерирует все требуемые мипмапы для текущей привязанной текстуры.
После окончания генерации текстуры и мипмапов хорошей практикой является освобождение участка памяти, выделенного под загруженное изображение, и отвязка объекта текстуры.
SOIL_free_image_data(image); glBindTexture(GL_TEXTURE_2D, 0);
Весь процесс генерации текстуры выглядит примерно так:
GLuint texture; glGenTextures(1, &texture); glBindTexture(GL_TEXTURE_2D, texture); // Устанавливаем настройки фильтрации и преобразований (на текущей текстуре) . // Загружаем и генерируем текстуру int width, height; unsigned char* image = SOIL_load_image("container.jpg", &width, &height, 0, SOIL_LOAD_RGB); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image); glGenerateMipmap(GL_TEXTURE_2D); SOIL_free_image_data(image); glBindTexture(GL_TEXTURE_2D, 0);
Применение текстур
Для последующих глав мы будем использовать четырехугольник отрисованный с помощью *glDrawElements из последней части урока про Hello Triangle. Нам надо сообщить OpenGL как сэмплировать текстуру, поэтому мы обновим вершинные данные, добавив в них текстурные координаты:
GLfloat vertices[] = < // Позиции // Цвета // Текстурные координаты 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // Верхний правый 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // Нижний правый -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // Нижний левый -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // Верхний левый >;
После добавления дополнительных атрибутов нам снова придется оповестить OpenGL о нашем новом формате:
.
glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat))); glEnableVertexAttribArray(2);
Заметьте, что мы также скорректировали значение шага прошлых двух атрибутов под 8 * sizeof(GLfloat).
Затем нам потребуется изменить вершинный шейдер для того, чтобы он принимал текстурные координаты в качестве атрибута а затем передавал их фрагментному шейдеру:
#version 330 core layout (location = 0) in vec3 position; layout (location = 1) in vec3 color; layout (location = 2) in vec2 texCoord; out vec3 ourColor; out vec2 TexCoord; void main()
Фрагментный шейдер также должен принимать TexCoord в качестве входной переменной.
Фрагментный шейдер также должен иметь доступ к текстурному объекту, но как мы передадим его во фрагментный шейдер? GLSL имеет встроенный тип данных для текстурных объектов, называемый sampler у которого в качестве окончания тип текстуры, тоесть sampler1D, sampler3D и, в нашем случае, sampler2D. Мы можем добавить текстуру фрагментному шейдеру просто объявив uniform smpler2D к которому мы позже передадим текстуру.
#version 330 core in vec3 ourColor; in vec2 TexCoord; out vec4 color; uniform sampler2D ourTexture; void main()
Для сэмплирования цвета текстуры мы используем встроенную в GLSL функцию texture которая в качестве первого аргумента принимает текстурный sampler, а в качестве второго аргумента текстурные координаты. Функция texture затем сэмплирует значение цвета, используя текстурные параметры, которые мы задали ранее. Результатом работы этого фрагментного шейдера будет (фильтрованный) цвет текстуры на (интерполированноый) текстурной координате.
Осталось только привязать текстуру перед вызовом glDrawElements и она автоматически будет передана сэмплеру фрагментного шейдера:
glBindTexture(GL_TEXTURE_2D, texture); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); glBindVertexArray(0);
Если вы все сделали верно то получите следующее изображение:
Если ваш четырехугольник полностью черный или белый значит вы где-то ошиблись. Проверьте шейдерные логи и сравните ваш код с исходным.
Для получения более цветастого эффекта мы можем смешать результирующий цвет текстуры с вершинным цветом. Для смешивания мы просто умножим цвета во фрагментном шейдере.
Color = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0f);
У вас должно получиться нечто такое?
Текстурный блок
Возможно вы задаетесь вопросом: “Почему sampler2D переменная является uniform, если мы ей так и не присвоили никакое значение с помощью glUniform?”. С помощью glUniform1i мы можем присвоить значение метоположения текстурному сэмплеру для возможности использования нескольких текстур в одном фрагментном шейдере. Местоположение текстуры чаще называется текстурным блоком. Текстурный блок по умолчанию — 0, который означает текущий активный текстурный блок для того, чтобы нам не требовалось указывать местоположение в прошлой секции.
Основная цель текстурных блоков это обеспечение возможности использования более чем 1 текстуры в нашем шейдере. Передавая текстурные блоки сэмплеру мы можем привязывать несколько текстур за один раз до тех пор, пока мы активируем соотносящиеся текстурные блоки. Также как и glBindTexture мы можем активировать текстуры с помощью glActivateTexture передавая туда текстурный блок, который мы хотим использовать:
glActiveTexture(GL_TEXTURE0); // Активируем текстурный блок перед привязкой текстуры glBindTexture(GL_TEXTURE_2D, texture);
После активации текстурного блока, последующий вызов glBindTexture привяжет эту текстуру к активному текстурному блоку. Блок GL_TEXTURE0 всегда активирован по-умолчанию, так что нам не требовалось активировать текстурные блоки в прошлом примере.
OpenGL поддерживает как минимум 16 текстурных блоков, которые вы можете получить через GL_TEXTURE0 — GL_TEXTURE15. Они объявлены по-порядку, поэтому вы также можете получить их следующим образом: GL_TEXTURE8 = GL_TEXTURE0 + 8. Это удобно, если вам приходится итерировать через текстурные блоки.
В любом случае нам все еще требуется изменить фрагментный шейдер для принятия другого сэмплера:
#version 330 core . uniform sampler2D ourTexture1; uniform sampler2D ourTexture2; void main()
Финальный результат — это комбинация двух текстур. В GLSL встроена функция mix которая принимает два значения на вход и интерполирует их на основе третьего значения. Если третье значение 0.0 то эта функция вернет первый аргумент, если 1.0 то второй. Значение в 0.2 вернет 80% первого входного цвета и 20% второго входного цвета.
Теперь нам надо загрузить и создать другую текстуру; вы уже знакомы со следующими шагами. Удостоверьтесь, что вы создали еще один объект текстуры, загрузили изображение и сгенерировали финальную текстуру с помощью glTexImage2D. Для второй текстуры мы используем изображение лица во время изучения этих уроков.
Для того, чтобы использовать вторую текстуру (и первую) нам надо будет немного изменить процедуру отрисовки, привязкой обеих текстур к соответствующим текстурным блокам и указанием к какому сэмплеру относится какой текстурный блок:
glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture1); glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture1"), 0); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, texture2); glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture2"), 1); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); glBindVertexArray(0);
Заметьте, что использовали glUniform1i для того, чтобы установить позицию текстурного блока в uniform sampler. Устанавливая их через glUniform1i мы будем уверены, что uniform sampler соотносится с правильным текстурным блоком. В результате вы должны будете получить следующий результат:
Вероятно вы заметили, что текстура перевернута вверх ногами! Это произошло, поскольку OpenGL представляет координату 0.0 по оси Y снизу изображения, но изображения зачастую имеют координату 0.0 сверху по оси Y. Некоторые библиотеки для загрузки изображений, типа Devil имеют настройки для инвертирования Y оси во время загрузки. SOIL такой настройки лишен. У SOIL есть функция SOIL_load_OGL_texture, которая загружает текстуру и генерирует текстуру с флагом SOIL_FLAG_INVERT_Y, который решает нашу проблему. Тем не менее эта функция использует вызовы, недоступные в современной версии OpenGL, поэтому нам придется остановиться на использовании SOIL_load_image и самостоятельной загрузкой текстуры.
Для исправления этой небольшой недоработки у нас есть 2 пути:
- Мы можем изменить текстурные координаты в вершинных данных и перевернуть Y ось (вычесть Y координату из 1)
- Мы можем изменить вершинный шейдер для переворачивания Y координаты, заменив формулу задачи TexCoord на TexCoord = vec2(texCoord.x, 1.0f — texCoord.y);..
Приведенные решения — это маленькие хаки, которые позволяют перевернуть изображение. Эти способы работают в большинстве случаев, но результат всегда будет зависеть от формата и типа выбираемой текстуры, так что лучшее решение проблемы — решать ее на этапе загрузки изображения, приводя ее в формат, понятный OpenGL.
Как только вы измените вершинные данные или перевернете Y ось в вершинном шейдере вы получите следующий результат:
Если вы увидели счастливый контейнер, то вы все сделали правильно. Вы можете сравнить свой код с исходным, а также вершинный и фрагментный шейдеры.
Упражнения
Для лучшего усвоения материала прежде чем приступать к следующему уроку взгляните на следующие упражнения.
- Добейтесь того, чтобы только вершинный шейдер был перевернут, с помощью изменения фрагментного шейдера. Решение
- Поэкспериментируйте с другим методам натягивания текстур, изменяя текстурные координаты в пределах от 0.0f до 2.0f вместо 0.0f до 1.0f. Проверьте, сможете ли вы отобразить 4 улыбающихся рожицы на одном контейнере. Решение, Результат
- Попробуйте отобразить только центральные пиксели текстуры на четырехугольнике так, чтобы единичные пиксели были видны при изменении текстурных координат. Попробуйте установить режим фильтрации GL_NEAREST для того, чтобы было видно пиксели более четко. Решение.
- Используйте uniform переменную в качестве 3 параметра функции mix для изменения коэффициента смешивания двух текстур на лету. Используйте кнопки вверх и вниз для регулирования смешивания. Решение, Фрагментный шейдер
Уроки OpenGL
Знание устройства bmp формата, это совсем не обязательное знание, так каждая вторая библиотека по работе с графикой умеет загружать bmp. Но так как это очень просто и может помочь нам понять как это работает внутри, мы сделаем этот велосипед. Мы напишем загрузчик BMP файлов с нуля чтобы понять как он работает и забудем про него.
Прототип функции загружающей текстуру:
GLuint loadBMP_custom(const char * imagepath);
И эту функцию можно будет вызывать вот так:
GLuint image = loadBMP_custom(«./my_texture.bmp»);
Теперь давайте разберем, как читать BMP файл.
Сначала нам нужно прочитать некоторые данные. Нужно установить следующие переменные при чтении BMP файла:
unsigned char header [54]; // каждый BMP файл начинается с 54байтного заголовка
unsigned int dataPos ; // Позиция в файле где сами данные начинаются
unsigned int width, height;
unsigned int imageSize; // = ширина * высота *3
// Сами RGB данные
unsigned char * data;
Сначала нам нужно открыть файл:
// Открываем файл
FILE * file = fopen(imagepath,»rb»);
if (!file
BMP файл начинается с 54 байтного заголовка. Этот заголовок содержит такую информацию как: «Это действительно BMP файл?», размер изображения, количество битов на пиксель, итд. Давайте сначала прочтем заголовок :
if ( fread ( header , 1, 54, file )!=54 )< // У нас проблемы, если не смогли прочитать 54 байта
printf(«Not a correct BMP file\n»);
return false;
>
Каждый BMP файл начинается с заголовка BM. Это можно увидеть о т крыв BMP файл в HEX редакторе:
И так нам нужно проверить первые два символа:
if(header[0]!=’B’ || header[1]!=’M’ )
printf(«Not a correct BMP file\n»);
return 0;
>
Теперь когда заголовок у нас прочитан, мы можем получить смещение в файле на данные, и размер картинки:
// Читаем int из массива байтов
dataPos = *(int*)&(header[0x0A]);
imageSize = *(int*)&(header[0x22]);
width = *(int*)&(header[0x12]);
height = *(int*)&(header[0x16]);
в некоторых BMP файлах нет полной информации, поэтому мы её добавим сами:
// в некоторых BMP файлах нет полной информации, поэтому мы её добавим сами
if ( imageSize ==0) imageSize = width * height *3; // 3 : Один байт на каждую Red, Green, Blue компоненты
if ( dataPos ==0) dataPos =54; // Тут заканчивается заголовок, и по идее, должны начаться данные
Теперь когда у нас есть вся необходимая информация, мы можем выделить память под картинку и прочитать в неё данные:
// Создаем буфер
data = new unsigned char [imageSize];
// Читаем данные из файла в буфер
fread(data,1,imageSize,file);
//Теперь все данные в памяти, и можно закрыть файл
fclose(file);
Но это все было не совсем связанное с OpenGL, а теперь будет поинтереснее. Создание текстур, это почти то же самое, что и создание вершинных буферов: создаем текстуру, биндим её, заполняем и конфигурируем.
В функции glTexImage2D,GL_RGB указывает на то, что мы работаем с трехкомпонентным цветом, а GL_BGR указывает на то, как этот цвет хранится в памяти. По какой-то исторической причине в BMP файлах цвет хранится не как Red-Green-Blue, а как Blue-Green-Red, о чем и нужно уведомить OpenGL.
// Создаем одну OpenGL текстуру
GLuint textureID;
glGenTextures(1, &textureID);
// Биндим текстуру, и теперь все функции по работе с текстурами будут работать с этой
glBindTexture(GL_TEXTURE_2D, textureID);
// Отправляем картинку в OpenGL текстуру
glTexImage2D(GL_TEXTURE_2D, 0,GL_RGB, width, height, 0, GL_BGR, GL_UNSIGNED_BYTE, data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
последние две строчки мы разберем позже. А пока, после всего этого, грузить текстуры можно будет так:
GLuint Texture = loadBMP_custom(«uvtemplate.bmp»);
Еще одно важное замечание: Используйте текстуры со значениями ширины и высоты как степень двойки:
- хорошо : 128*128*, 256*256, 1024*1024, 2*2…
- плохо : 127*128, 3*5, …
- хорошо, но странно : 128*256
Использование текстур в OpenGL
Давайте посмотрим на фрагментный шейдер который будет выводить нашу текстуру.
#version 330 core
// Интерполированное значение из вершинного шейдера
in vec2 UV;
// Выходное значение
out vec3 color;
// Константа которая будет одинакова на протяжении обработки всего меша
uniform sampler2D myTextureSampler;
void main()
// Результирующий цвет — цвет точки в текстуре по координатам UV
color = texture( myTextureSampler, UV ).rgb;
>
Три вещи на которые нужно обратить внимание:
- Фрагментный шейдер нуждается в UV координатах
- Нам нужен так называемый сэмплер(sampler2D), чтобы шейдер знал из какой текстуры извлекать цвет
- Доступ к цвету фрагмента из текстуры происходит с помощью функции texture() которая возвращает нам цвет в формате (R,G,B,A) vec4. Вскоре мы рассмотрим что такое A компонента.
Вершинный шейдер тоже очень простой:
#version 330 core
// Входные данные о вершинах разные при каждом вызове шейдера
layout(location = 0) in vec3 vertexPosition_modelspace;
layout(location = 1) in vec2 vertexUV;
// Исходящие данные: будут интерполированы для каждого фрагмента
out vec2 UV;
// Значение которое будет оставаться константой для всего меша
uniform mat4 MVP;
void main()
// Результирующая позиция в пространстве отсечения : МВП * положение
gl_Position = MVP * vec4(vertexPosition_modelspace,1);
// координата UV данной вершины. Тут никаких особых преобразований не нужно делать.
UV = vertexUV;
>
Что такое “layout(location = 1) in vec2 vertexUV”? Если забыли, посмотрите в урок 4. В этом уроке мы будем делать почти то же самое, вот только вместо триплетов цвета, мы передадим в шейдер пары координат UV:
// На каждую вершину по две UV координаты. Я их создал с помощью Blender.
Вскоре вы узнаете как генерировать их самостоятельно.
static const GLfloat g_uv_buffer_data[] =
0.000059f, 1.0f-0.000004f,
0.000103f, 1.0f-0.336048f,
0.335973f, 1.0f-0.335903f,
1.000023f, 1.0f-0.000013f,
0.667979f, 1.0f-0.335851f,
0.999958f, 1.0f-0.336064f,
0.667979f, 1.0f-0.335851f,
0.336024f, 1.0f-0.671877f,
0.667969f, 1.0f-0.671889f,
1.000023f, 1.0f-0.000013f,
0.668104f, 1.0f-0.000013f,
0.667979f, 1.0f-0.335851f,
0.000059f, 1.0f-0.000004f,
0.335973f, 1.0f-0.335903f,
0.336098f, 1.0f-0.000071f,
0.667979f, 1.0f-0.335851f,
0.335973f, 1.0f-0.335903f,
0.336024f, 1.0f-0.671877f,
1.000004f, 1.0f-0.671847f,
0.999958f, 1.0f-0.336064f,
0.667979f, 1.0f-0.335851f,
0.668104f, 1.0f-0.000013f,
0.335973f, 1.0f-0.335903f,
0.667979f, 1.0f-0.335851f,
0.335973f, 1.0f-0.335903f,
0.668104f, 1.0f-0.000013f,
0.336098f, 1.0f-0.000071f,
0.000103f, 1.0f-0.336048f,
0.000004f, 1.0f-0.671870f,
0.336024f, 1.0f-0.671877f,
0.000103f, 1.0f-0.336048f,
0.336024f, 1.0f-0.671877f,
0.335973f, 1.0f-0.335903f,
0.667969f, 1.0f-0.671889f,
1.000004f, 1.0f-0.671847f,
0.667979f, 1.0f- 0.335851f
>;
Координаты которые я привел выше относятся к следующей модели:
Остальное, я думаю, должно быть для вас уже очевидным. Генерируем буфер, биндим его, заполняем и конфигурируем. Обычный буфер, все как всегда. Не забудьте только использовать 2 в качестве параметра size функции glVertexAttribPointer вместо 3.
А вот и результат:
И увеличенная версия:
Фильтрация, Мипмаппинг, и как этим всем пользоваться
Как видно из предыдущего увеличенного скриншота, качество текстуры не сильно хорошее. Но это потому что в загрузчике текстуры мы написали:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
Это значит, что наш фрагментный шейдер в функции texture() просто берет значение текселя по координатам UV и выводит его на экран:
Но у нас есть в запасе несколько фокусов которые помогут улучшить картинку.
Линейная фильтрация
Когда включена линейная фильтрация, функция texture() смотрит не только на один конкретный тексель, но и на несколько вокруг него и смешивает их цвета. Из-за чего у нас пропадает некрасивые четкие зубцы в текстуре:
С такой фильтрацией все становится немножко получше, но есть еще один метод — Анизотропная фильтрация, который дает еще более красивую картинку. Но он значительно медленнее.
Анизотропная фильтрация
По этому методу мы аппроксимируем картинку которая действительно должна быть видима через фрагментный шейдер. Например, если мы будем смотреть на эту текстуру сбоку и немного повернуто, то анизотропная фильтрация будет вычислять цвет расположенный в голубом прямоугольнике беря фиксированное число выборок(анизотропный уровень) вдоль основного направления.
Мипмаппинг(MipMaps)
И у линейной и у анизотропной фильтрации есть проблема. Если текстура видна из далека, то смешивания четырех смежных текселей не поможет. Например наша 3д модель может быть расположена так далеко, что она будет занимать всего один пиксель на экране. В таком случае результирующая точка должна быть усредненным цветом всех текселей текстуры. Но видеокарта этого не будет делать из-за того, что это будет слишком долго. Вместо этого придумали другой метод — мипмаппинг:
- Во время загрузки текстуры мы изменяем её размер в два раза, потом результирующую еще в два раза итд пока у нас не получится текстура 1х1 (все промежуточные сохраняем, конечно же).
- Когда рисуем модель, то видеокарта выбирает наиболее подходящую текстуру среди мипмапов в зависимости от того, каким должен быть тексель по размеру.
- Делать выборку текселя из подходящего мипмапа можно любым способом, хоть nearest хоть linear хоть anisotropic.
- Если хочется еще лучшего качества, можно попробовать делать выборку из двух ближайших мипмапов и смешивать полученные цвета.
К нашему счастью все эти алгоритмы нам делать заново не нужно. OpenGL все умеет делать сам если его хорошенько попросить.
// Когда картинка будет увеличиваться(нет большей Мипмапы), используем LINEAR фильтрацию
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// Когда минимизируем — берем две ближних мипмапы и лиейно смешиваем цвета
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
// И создаем сами мипмапы.
glGenerateMipmap(GL_TEXTURE_2D);
Загрузка текстуры с помощью GLFW
Наша функция для загрузки BMP файлов очень хороша, но использовать готовую проверенную годами библиотеку загрузки изображений, гораздо лучше. GLFW, например, умеет хорошо загружать TGA файлы.
как добавить на куб разные текстуры
Здравствуйте. Я хочу нарисовать куб в openGL, у которого на каждой грани — своя текстура. Вот такой вот рабочий пример куба у которого одна текстура используется для обтягивания всех его граней:
public class Cube extends Abstract3DFigure < private FloatBuffer vertexBuffer; private FloatBuffer textureBuffer; private ByteBuffer indexBuffer; private int[] textures = new int[6]; private float vertices[] = < //Vertices according to faces -1.0f, -1.0f, 1.0f, //Vertex 0 1.0f, -1.0f, 1.0f, //v1 -1.0f, 1.0f, 1.0f, //v2 1.0f, 1.0f, 1.0f, //v3 1.0f, -1.0f, 1.0f, //. 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, >; private float texture[] = < //Mapping coordinates for the vertices 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, >; private byte indices[] = < //Faces definition 0, 1, 3, 0, 3, 2, //Face front 4, 5, 7, 4, 7, 6, //Face rightButtonPositiion 8, 9, 11, 8, 11, 10, //. 12, 13, 15, 12, 15, 14, 16, 17, 19, 16, 19, 18, 20, 21, 23, 20, 23, 22, >; public Cube(Context context) < ByteBuffer byteBuf = ByteBuffer.allocateDirect(vertices.length * 4); byteBuf.order(ByteOrder.nativeOrder()); vertexBuffer = byteBuf.asFloatBuffer(); vertexBuffer.put(vertices); vertexBuffer.position(0); byteBuf = ByteBuffer.allocateDirect(texture.length * 4); byteBuf.order(ByteOrder.nativeOrder()); textureBuffer = byteBuf.asFloatBuffer(); textureBuffer.put(texture); textureBuffer.position(0); indexBuffer = ByteBuffer.allocateDirect(indices.length); indexBuffer.put(indices); indexBuffer.position(0); >public void draw(GL10 gl) < gl.glPushMatrix(); gl.glTranslatef(0, 0, -10); //Move 5 units into the screen gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); //Point to our buffers gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY); //Set the face rotation gl.glFrontFace(GL10.GL_CCW); //Enable the vertex and texture state gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer); gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer); //Draw the vertices as triangles, based on the Index Buffer information gl.glDrawElements(GL10.GL_TRIANGLES, indices.length, GL10.GL_UNSIGNED_BYTE, indexBuffer); gl.glPopMatrix(); >public void loadGLTexture(GL10 gl, Context context) < Bitmap bitmap = loadResurse(context, R.drawable.border); //Generate one texture pointer. gl.glGenTextures(1, textures, 0); //. and bind it to our array gl.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); //Create Nearest Filtered Texture gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); //Different possible texture parameters, e.g. GL10.GL_CLAMP_TO_EDGE gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_REPEAT); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_REPEAT); //Use the Android GLUtils to specify a two-dimensional texture image from our bitmap GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0); //Clean upButtonPositiion bitmap.recycle(); >private Bitmap loadResurse(Context context, int resourceId) < InputStream is = context.getResources().openRawResource(resourceId); Bitmap bitmap = null; try < //BitmapFactory is an Android graphics utility for images bitmap = BitmapFactory.decodeStream(is); >finally < //Always clear and close try < is.close(); >catch (IOException e) < >is = null; > return bitmap; > >
Мне бы хотелось использовать для разных граней — разные текстуры. В данном коде я гружу только одну текстуру. Bitmap bitmap = loadResurse(context, R.drawable.border) ;
А нужно 6 текстур, я могу их получить таким образом, но как потом их натянуть на разные грани мне не понятно.
Bitmap east = loadResurse(context, R.drawable.east); Bitmap north = loadResurse(context, R.drawable.north); Bitmap west = loadResurse(context, R.drawable.west); Bitmap south = loadResurse(context, R.drawable.south); Bitmap up = loadResurse(context, R.drawable.up); Bitmap down = loadResurse(context, R.drawable.down);
Как я могу это сделать ?