Войти
ПрограммированиеСтатьиГрафика

OpenGL на Qt 4. Это просто! (часть 2) (3 стр)

Автор:

Текстуры

Современная графика просто немыслима без использования текстур. Текстуры создают наиболее реалистичное изображение, будь то фрагмент кирпичной стены, деревянная поверхность или тканевое полотно. В этом разделе мы познакомимся с простым наложением текстур. Нужно знать, что текстура измеряется в текселях. Тексель — это как бы минимальная единица текстуры, как бы исходный пиксель текстуры. При увеличении этот исходный пиксель превращается в квадрат.

Инициализация текстур

Для того, чтобы использовать текстуры, вам прежде всего потребуется установить состояние OpenGL для работы с текстурами. Как и любая установка это происходит с помощью команды glEnable() с параметром GL_TEXTURE_2D. Параметр GL_TEXTURE_2D означает, что вы будете работать только с двумерной картой текстуры, или двумерной текстурой. Двумерная карта является самой распространённой и интуитивно понятной. Например, есть двумерная поверхность (пусть прямоугольник) и на неё накладывается двумерное изображение (пусть ваша фотография с отпуска). Существуют ещё одномерные и (ого!) трёхмерные карты текстуры. Не стоит пугаться стольких размерностей. Они лишь означают, сколько координат имеет точка текстуры: одну, две или (даже) три. Ёще раз вспомните любую картинку (может быть, свои фотографии с отпуска) и вы поймете, что в нашем обычном понимании на картинке таких координат две: x и y (ширина и высота, их официально принято называть s и t). Чуть позже мы вернёмся к вопросу о других «размерностях».

Установить работу с текстурами логичнее всего при инициализации OpenGL. Пример инициализации текстур:

void Scene3D::initializeGL() // инициализация
{
   // другие настройки ...

   // ...
   genTextures(); // создать текстуры
   glEnable(GL_TEXTURE_2D); // установить режим двумерных текстур

   // ...
   getTextureArray(); // определить массив текстурных координат вершин
   
   // ...
   // активизируем массив текстурных координат
   glEnableClientState(GL_TEXTURE_COORD_ARRAY);
}

Первой функцией genTextures() мы создаём текстуры. Пока ограничимся той простой информацией, что в этой функции, в частности, загружаются изображения для текстур из графических файлов. Мы подробно вернемся к этой функции в следующем подразделе. Как уже говорилось, glEnable(GL_TEXTURE_2D) устанавливает режим двумерной катры текстур, или короче двумерных текстур. Далее идёт определение getTextureArray() и активация glEnableClientState(GL_TEXTURE_COORD_ARRAY) массива текстурных координат. В функции getTextureArray() определяется массив текстурных координат вершин, аналогично как определяется сам массив вершин. Их даже удобно определять вместе в одной функции. Если массив вершин содержит три координаты для каждой вершины в пространстве: x, y, z; то массив текстурных координат — две координаты для каждой вершины: s и t (мы их будем тоже обозначать как x и y для наглядности). Команда glEnableClientState(GL_TEXTURE_COORD_ARRAY) активирует массив текстурных координат вершин. Вспомним, что, например, эта же команда, но с другим параметром GL_VERTEX_ARRAY, активирует массив вершин. Здесь подразумевается, что она спрятана в комментарии (//...). При работе с массивами вершин нужно вызывать обе команды.

Создание текстур

Для того, чтобы создать текстуру, нужно выполнить всего три действия:
1) загрузить изображение;
2) создать уникальное имя для текстурного объекта;
3) создать текстурный объект и связать его с состоянием текстуры.

Текстурный объект содержит в себе всю необходимую информацию относительно текстуры. Конкретный текстурный объект однозначно идентифицируется числом типа GLuint, аналогично как, например, идентифицируется дисплейный список. Это число есть имя текстурного объекта. После того, как создан текстурный объект, его нужно связать с его состоянием. Состояние текстурного объекта включает в себя само изображение, которое используется как текстура, и то, как оно выводится.

Сначала разберём низкоуровневое создание текстур. Пример создания текстур:

//...

// декларируем массив текстурных объектов (ID текстур)
GLuint textureID[2]; // размер равен двум, в программе будет всего два текстурных объекта

//...

void Scene3D::genTextures() // функция genTexture() класса Scene3D, создаёт текстуры
{
   // загрузка изображений
   QImage image1, image2; // создаём объекты класса QImage (изображения)
   image1.load("../textures/picture1.jpg"); // загружаем изображение в переменную image1
   image2.load("../textures/picture2.jpg"); // загружаем изображение в переменную image2
   // конвертируем изображение в формат для работы с OpenGL:
   image1=QGLWidget::convertToGLFormat(image1); 
   image2=QGLWidget::convertToGLFormat(image2);

   // создание имён для текстурных объектов
   glGenTextures(2, textureID); // создаём два имени и записываем их в массив

   // создаём и связываем текстурные объекты с состоянием текстуры
   // 1-ый текстурный объект
   // создаём и связываем 1-ый текстурный объект с последующим состоянием текстуры  
   glBindTexture(GL_TEXTURE_2D, textureID[0]); 
   // связываем текстурный объект с изображением 
   glTexImage2D(GL_TEXTURE_2D, 0, 3, (GLsizei)image1.width(), (GLsizei)image1.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image1.bits());

   // 2-ой текстурный объект
   // создаём и связываем 2-ой текстурный объект с последующим состоянием текстуры   
   glBindTexture(GL_TEXTURE_2D, textureID[1]);
   // связываем текстурный объект с изображением
   glTexImage2D(GL_TEXTURE_2D, 0, 3, (GLsizei)image2.width(), (GLsizei)image2.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image2.bits());
   // дополнительные параметры текстурного объекта
   // задаём линейную фильтрацию вблизи:
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
   // задаём линейную фильтрацию вдали:
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
   // задаём: при фильтрации игнорируются тексели, выходящие за границу текстуры для s координаты
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
   // задаём: при фильтрации игнорируются тексели, выходящие за границу текстуры для t координаты
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 
   // задаём: цвет текселя полностью замещает цвет фрагмента фигуры
   glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
}

В примере создано два текстурных объекта (или просто две текстуры), которые используют изображения picture1.jpg и picture2.jpg из папки textures. "../" в начале адреса означает подняться на одну директорию выше. В системе Windows XP папка textures будет искаться на директорию выше, чем исполняемый файл. Если вы хотите, чтобы папка textures искалась в той же директории, где находится исполняемый файл, измените адреса на "textures/picture1.jpg" и "textures/picture2.jpg". В openSUSE 11.2 папка textures по умолчанию будет искаться в "Домашней папке" ("../textures/picture1.jpg") или же в "Домашней папке" в "Документах" ("textures/picture1.jpg"). Загрузка изображения происходит средствами Qt с помощью класса QImage для работы с изображениями. Затем изображения нужно преобразовать к формату OpenGL с помощью convertToGLFormat() класса QGLWidget. Растровое изображение (т.е. прямоугольное изображение из пикселей) записано в память в виде массива данных и обращение к нему должно происходить через указатель на этот массив.

Важная особенность текстур (загружаемого изображения) состоит в том, что их размеры (ширина и высота) должны быть целыми степенями двойки, но ширина и высота могут быть и не равны друг другу (например, 256x256, или 512x512, или же 512x256). Массив textureID хранит в себе уникальные номера текстурных объектов, которые нужны для их идентификации. Другими словами, каждая текстура должна иметь свой уникальный номер, по которому к ней следует обращаться. Это легко понять: как вы имеете свой паспорт или ИНН для идентификации вашей личности, так и текстура имеет свой номер для идентификации себя. Как раз функция glGenTextures() генерирует такие уникальные имена в виде целых чисел без знака (типа GLuint). Эти числа записываются в массив textureID. Совсем необязательно, что такие целые числа будут последовательными. Вообще говоря, числа можно не генерировать, а задавать их самостоятельно; но всё же лучше их создавать автоматически, так как в очень больших программах держать в голове все номера текстур просто невозможно.

До этого момента мы создали только уникальные имена и ни с чем их ещё не связали. Текстурные объекты создаёт функция glBindTexture(), а не glGenTextures(), как можно было бы подумать из названия последней. Создание текстурного объекта и связывание с состоянием текстуры происходит с помощью функции glBindTexture(GL_TEXTURE_2D, textureID[0]) с двумя параметрами: первый сообщает, с какой текстурой мы работаем (двумерная текстура); второй параметр есть уникальный номер (в этом случае просто элемент массива). Все последующие заданные состояния текстуры теперь относятся к этому текстурному объекту. Первым делом нужно указать в состоянии текстуры, какой рисунок нужно загрузить в неё из буфера памяти. Это делается с помощью функции glTexImage2D() с довольно большим числом параметров. Первый параметр и так понятен (хотя он не единственен для двумерной текстуры). Второй параметр 0 соответствует уровню сокращённой текстуры. В данном случае 0 означает, что текстура является несокращённой. Суть сокращения текстуры (mipmap, или MIP-текстуры, или множественного отображения) заключается в том, что в зависимости от удаления от примитива, на который наложена текстура, можно использовать рисунки разных размеров (разных разрешений). Чем ближе наблюдатель к объекту, тем больше используемая текстура; чем дальше, тем меньше. Мы не будем подробно останавливаться на сокращённых текстурах. Следующий параметр 3 означает, что на тексель выделяется 3 компоненты цвета RGB. Следующие два параметра есть ширина и высота текстуры в пикселях (текселях). Конечно, можно задать их вручную, но гораздо удобнее определить автоматически. Для этого мы воспользовались методами width() и height() класса QImage. Следующий параметр 0 относится к границе текстуры, которую можно расширить на дополнительную ширину или высоту. Это дополнительное приращение к ширине и высоте называется толщиной границы и может быть равно либо 0, либо 1. Нуль в нашем случае означает, что мы ничего не расширяем. Затем параметры GL_RGBA и GL_UNSIGNED_BYTE сообщают информацию OpenGL относительно пикселей и типа данных для текселей. Последний параметр есть указатель на начало массива данных. Для этой цели мы используем функцию bits() класса QImage. Хотя формат объектов image1 и image2 изменён и уже не является как таковым типом QImage, для него всё равно применимы методы width(), height() и bits() класса QImage. Рисунок текстуры можно также обновить (заместить) с помощью glTexSubImage2D(), для получения более подробной информации отсылаем читателя к литературе по OpenGL.

После функции glBindTexture() текстура становится текущей не только для установки состояния текстуры, но и для наложения. Т.е. текущая текстура будет использоваться при последующем наложении. Поэтому glBindTexture() нужно использовать для выбора конкретной текстуры для наложения, т.е. для переключения между текстурами. Далее в функциях glTexParameteri() задана фильтрация текстуры вблизи и вдали. Параметром GL_LINEAR установлена линейная фильтрация, которая будет сглаживать изображение. Попробуйте изменить линейную фильтрацию GL_LINEAR на фильтрацию по ближайшему соседу GL_NEAREST. Тогда текстурная координата примет цвет ближайшего к ней текселя и в результате вы увидите при увеличении большие тексели-квадраты. При линейной фильтрации на стыке текстуры могут возникнуть заметные полосы или артефакты. Так происходит, поскольку при использовании линейной фильтрации цвет рассчитывается исходя из окружения текстурной координаты текселями вокруг неё. Поэтому цвет на границе по умолчанию формируется за счет текселей, лежащих по другую сторону текстуры, которые в свою очередь могут значительно отличаться по цвету. Командой glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) мы указываем, чтобы игнорировались тексели, выходящие за границу текстуры для s-координаты (или x). Т.е. тексели по другую сторону текстуры просто не будут учитываться при фильтрации. Соответственно, параметр GL_TEXTURE_WRAP_T для t-координаты (или y). Сам параметр GL_CLAMP_TO_EDGE (соответствующий игнорированию) относится к режиму намотки текстуры, который определяет, как работать с текстурными координатами, выходящими за диапазон [0,1]. Наконец, командой glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) устанавливается, что цвета текстуры полностью замещают цвета фрагмента фигуры. Например, при параметре GL_MODULATE (вместо GL_REPLACE) цвета вычисляются умножением цветов текстуры и фрагмента фигуры.

Теперь разберём высокоуровневое создание текстур с помощью Qt. Qt может самостоятельно создавать и связывать текстуры без использования glGenTextures(), явного использования glBindTexture() и использования glTexImage2D(). Пример:

// ...
 
GLuint textureID[2]; // размер равен двум, в программе будет всего два текстурных объекта
 
// ...
void Scene3D::genTexture() // функция genTexture() класса Scene3D, создаёт текстуры
{ 
   // создаём, связываем, загружаем, возвращаем уникальный номер: 
   textureID[0]=bindTexture(QPixmap(QString("../textures/picture1.jpg")), GL_TEXTURE_2D);
   // далее параметры текстурного объекта
   // при фильтрации игнорируются тексели, выходящие за границу текстуры для s координаты
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 
   // при фильтрации игнорируются тексели, выходящие за границу текстуры для t координаты
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
   // цвет текселя полностью замещает цвет фрагмента фигуры 
   glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); 

   // создаём, связываем, загружаем, возвращаем уникальный номер:
   textureID[1]=bindTexture(QPixmap(QString("../textures/picture2.jpg")), GL_TEXTURE_2D);
   // далее параметры текстурного объекта 
   // при фильтрации игнорируются тексели, выходящие за границу текстуры для s координаты
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
   // при фильтрации игнорируются тексели, выходящие за границу текстуры для t координаты
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 
   // цвет текселя полностью замещает цвет фрагмента фигуры
   glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); 
}

Функция bindTexture() класса QGLContext создаёт, связывает, загружает текстуру и возвращает уникальный номер текстуры. При этом установка текстуры текущей для наложения выполняется как и раньше с помощью glBindTexture(). При использовании bindTexture() по умолчанию устанавливается флаг состояния текстурного объекта QGLContext::DefaultBindOption, он уже включает в себя, например, линейную фильтрацию. Как вы догадываетесь: функция bindTexture() есть высокоуровневая надстройка над первым примером из этого подраздела.

Наложение текстур

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

demo | OpenGL на Qt 4. Это просто! (часть 2)

Как видно из иллюстрации, текстурные координаты (x,y) (координаты на текстуре) ставятся в соответствие с вершинами треугольника. Как уже говорилось, текстурные координаты обычно обозначают как s и t вместо x и y во избежание путаницы с пространственными координатами вершины; но мы будем использовать x и y, так как это более наглядно (но не забывайте, что официально они обозначаются как s и t). Сказанное можно пояснить на следующей схеме: (текстурные координаты вершины)-->(пространственные координаты вершины), т.е. (0,0)-->(x1,y1,z1), (1,0)-->(x2,y2,z2) и (0.5,1)-->(x3,y3,z3). В этом и заключается вся нехитрая технология наложения текстур. Если расстояния, которые образуются пространственными и текстурными координатами, непропорциональны друг другу, то текстуры будут искажаться: растягиваться, сжиматься, изгибаться; другими словами, наложенная текстура будет подстраиваться под вершины примитива.

При работе с функциями glBegin() и glEnd() наложение делается следующим образом:

// определены координаты вершин в пространстве:
// x1, y1, z1,
// x2, y2, z2, 
// x3, y3, z3
// ...

glBegin(GL_TRIANGLES)
   glTexCoord2f(0.0f, 0.0f); // текстурная координата вершины
   glVertex3f(x1, y1, z1);   // пространственная координата вершины
   
   glTexCoord2f(1.0f, 0.0f);
   glVertex3f(x2, y2, z2);

   glTexCoord2f(0.5f, 1.0f);
   glVertex3f(x3, y3, z3);

   // ...
glEnd()

Команды glBegin()/glEnd() сейчас устарели; вместо них используют массивы вершин. Хотя если требуется что-то построить, чтобы определить вид геометрии объектов, можно сделать это через glBegin()/glEnd(), а затем перенести фигуры на массивы. При работе с массивами вершин нужно определить массивы текстурных координат. Массивы вершин и текстурных координат даже удобно определить вместе в одной функции. Пример:

// продекларированы массивы:
// массив вершин: VertexArray типа GLfloat
// массив текстурных координат: TextureArray типа GLfloat
// массив индексов: IndexArray типа GLuint

// ...

// определяем массивы
{
   // определены координаты вершин в пространстве:
   // x1, y1, z1,
   // x2, y2, z2, 
   // x3, y3, z3

   // ...

   VertexArray[0][0]=x1; // пространственная x-координата вершины
   VertexArray[0][1]=y1; // пространственная y-координата вершины
   VertexArray[0][2]=z1; // пространственная z-координата вершины
   TextureArray[0][0]=0.0f; // текстурная x-координата вершины (или s)
   TextureArray[0][1]=0.0f; // текстурная y-координата вершины (или t)

   VertexArray[1][0]=x2;
   VertexArray[1][1]=y2;
   VertexArray[1][2]=z2;
   TextureArray[1][0]=1.0f;
   TextureArray[1][1]=0.0f;

   VertexArray[2][0]=x3;
   VertexArray[2][1]=y3;
   VertexArray[2][2]=z3;
   TextureArray[2][0]=0.5f;
   TextureArray[2][1]=1.0f;

   // ...
}

// определён массив индексов вершин IndexArray

// ...

// построение и рисование фигуры
{
   // указываем, откуда нужно извлечь данные о массиве вершин
   glVertexPointer(3, GL_FLOAT, 0, VertexArray);
   // указываем, откуда нужно извлечь данные о массиве текстурных координат
   glTexCoordPointer(2, GL_FLOAT, 0, TextureArray); 
   // строим поверхности
   glDrawElements(GL_TRIANGLES, IndexArraySize, GL_UNSIGNED_INT, IndexArray);
   // здесь IndexArraySize есть число элементов массива IndexArray, 
   // т.е. число треугольников умноженное на число вершин треугольника
}

Вернёмся к одномерным и трёхмерным текстурам. В случае одномерной текстуры её координаты имеют одно измерение. Такие текстуры можно, например, накладывать на линии. Как правило, непонимание вызывают трёхмерные текстуры, которые имеют три измерения. Текстурные координаты можно представить в виде точки в кубе; каждая из трёх текстурных координат лежит в диапазоне [0,1], в итоге и получается куб. Сама трёхмерная текстура представляет собой набор двумерных текстур, следующих друг за другом. То же самое, если бы трёхмерный массив представить набором двумерных массивов. Тексели в этом случае тоже становятся трёхмерными.

Удаление текстур

Для удаления текстурных объектов используется glDeleteTextures(), например, в случае двух текструных объектов: glDeleteTextures(2, textureID). Распространённая ошибка: при пересоздании сцены создавать текстурные объекты заново, но не удалять предыдущие; это приведёт к утечке памяти. Можно удалить текстурные объекты в деструкторе виджета:

Scene3D::~Scene3D() // деструктор
{
   // контекст рендеринга виджета станет текущим контекстом рендеринга OpenGL:
   makeCurrent();
   glDeleteTextures(2, textureID); // удаляем текстурные объекты
}

Вообще говоря, вызывать команды OpenGL можно не только в функциях initializeGL(), resizeGL() и paintGL(), но и в других местах; но в этом случае обязательно нужно указать, что контекст рендеринга виджета становится текущим контекстом рендеринга OpenGL с помощью функции makeCurrent() класса QGLWidget. Как раз это и происходит в деструкторе ~Scene3D(). Аналогом функции glDeleteTextures() служит deleteTexture() класса QGLContext, которая удаляет текстуру, созданную через bindTexture(). Например, текстурный объект id_1 можно удалить так: deleteTexture(id_1). При этом, если текстура была создана через bindTexture(), то в деструкторе уже необязательно вызывать deleteTexture().

Страницы: 1 2 3 4 5 6 Следующая »

#3D, #графика, #OpenGL, #Qt, #Qt4

10 августа 2011 (Обновление: 2 янв. 2013)

Комментарии [17]

http://qrz.ru/classifieds/category/hamradio 6rp купить рация.