Работа с OpenGL на Qt 4 (часть 2)

Материал из Wiki.crossplatform.ru

(Различия между версиями)
Перейти к: навигация, поиск
Строка 1: Строка 1:
Во 2-ой части продолжается рассмотрение работы с OpenGL на Qt 4. Читатель познакомится с настройкой контекста OpenGL, созданием анимации и простым наложением текстур. Заодно в рамках «чистого» OpenGL будет рассмотрена интерактивная графика с помощью режима выбора (выбираем объекты на экране для дальнейшей манипуляции с ними), в рамках «чистого» Qt будет продемонстрировано создание главного окна, меню (menu bar) и объяснён механизм сигналов и слотов.
Во 2-ой части продолжается рассмотрение работы с OpenGL на Qt 4. Читатель познакомится с настройкой контекста OpenGL, созданием анимации и простым наложением текстур. Заодно в рамках «чистого» OpenGL будет рассмотрена интерактивная графика с помощью режима выбора (выбираем объекты на экране для дальнейшей манипуляции с ними), в рамках «чистого» Qt будет продемонстрировано создание главного окна, меню (menu bar) и объяснён механизм сигналов и слотов.
-
Список частей:
+
'''Список частей:'''
* [http://www.wiki.crossplatform.ru/index.php/%D0%A0%D0%B0%D0%B1%D0%BE%D1%82%D0%B0_%D1%81_OpenGL_%D0%BD%D0%B0_Qt_4 Работа с OpenGL на Qt 4 (часть 1)]
* [http://www.wiki.crossplatform.ru/index.php/%D0%A0%D0%B0%D0%B1%D0%BE%D1%82%D0%B0_%D1%81_OpenGL_%D0%BD%D0%B0_Qt_4 Работа с OpenGL на Qt 4 (часть 1)]
* Работа с OpenGL на Qt 4 (часть 2)
* Работа с OpenGL на Qt 4 (часть 2)
 +
==Введение==
==Введение==
Строка 251: Строка 252:
Обратите внимание на макрос Q_OBJECT, который стоит вначале определения класса. Именно по этому макросу MOC (Meta Object Compiler — метаобъектный компилятор) определит, что в этом классе используется механизм сигналов и слотов и запишет соотвествующую информацию в специальный файл при препроцессорной обработке. Механизм сигналов и слотов может связывать между собой разные классы так, что объект одного класса может создавать сигнал, а объект другого класса получать этот сигнал и реагировать на него. В качестве слота класса Scene3D выступает слот change(), который будет изменять значения углов наблюдения (модельно-видовой матрицы) и обновлять изображение. В конструкторе класса Scene3D мы создаём динамический объект класса QTimer. Затем связываем объекты классов QTimer и нашего класса Scene3D (используем указатель this) с помощью функции connect() класса QObject. Сигнал-функция timeout() будет создавать сигналы таймера через определённый интервал времени. Наш объект this будет получать эти сигналы и реагировать на них вызовом слота-функции change(). Наконец, мы запускаем таймер с помощью start() с интервалом 30 миллисекунд. Обычно погрешность измерения времени операционными системами составляет 1 миллисекунду. Если вы ничего не задали в start() или задали 0, то таймер будет установлен на минимально возможный интервал. Если интервал таймера был уже установлен заранее для данного объекта класса QTimer, то функция start() запускает таймер с этим интервалом. Дополнительную информацию смотрите в Qt Assistant.
Обратите внимание на макрос Q_OBJECT, который стоит вначале определения класса. Именно по этому макросу MOC (Meta Object Compiler — метаобъектный компилятор) определит, что в этом классе используется механизм сигналов и слотов и запишет соотвествующую информацию в специальный файл при препроцессорной обработке. Механизм сигналов и слотов может связывать между собой разные классы так, что объект одного класса может создавать сигнал, а объект другого класса получать этот сигнал и реагировать на него. В качестве слота класса Scene3D выступает слот change(), который будет изменять значения углов наблюдения (модельно-видовой матрицы) и обновлять изображение. В конструкторе класса Scene3D мы создаём динамический объект класса QTimer. Затем связываем объекты классов QTimer и нашего класса Scene3D (используем указатель this) с помощью функции connect() класса QObject. Сигнал-функция timeout() будет создавать сигналы таймера через определённый интервал времени. Наш объект this будет получать эти сигналы и реагировать на них вызовом слота-функции change(). Наконец, мы запускаем таймер с помощью start() с интервалом 30 миллисекунд. Обычно погрешность измерения времени операционными системами составляет 1 миллисекунду. Если вы ничего не задали в start() или задали 0, то таймер будет установлен на минимально возможный интервал. Если интервал таймера был уже установлен заранее для данного объекта класса QTimer, то функция start() запускает таймер с этим интервалом. Дополнительную информацию смотрите в Qt Assistant.
 +
 +
==Текстуры==
 +
 +
Современная графика просто немыслима без использования текстур. Текстуры создают наиболее реалистичное изображение, будь то фрагмент кирпичной стены, деревянная поверхность или тканевое полотно. В этом разделе мы познакомимся с простым наложением текстур. Нужно знать, что текстура измеряется в текселях. Тексель — это как бы минимальная единица текстуры, как бы исходный пиксель текстуры. При увеличении этот исходный пиксель превращается в квадрат.
 +
 +
===Инициализация текстур===
 +
 +
Для того, чтобы использовать текстуры, вам прежде всего потребуется установить состояние OpenGL для работы с текстурами. Как и любая установка это происходит с помощью команды glEnable() с параметром GL_TEXTURE_2D. Параметр GL_TEXTURE_2D означает, что вы будете работать только с двумерной картой текстуры, или двумерной текстурой. Двумерная карта является самой распространённой и интуитивно понятной. Например, есть двумерная поверхность (пусть прямоугольник) и на неё накладывается двумерное изображение (пусть ваша фотография с отпуска). Существуют ещё одномерные и (ого!) трёхмерные карты текстуры. Не стоит пугаться стольких размерностей. Они лишь означают, сколько координат имеет точка текстуры: одну, две или (даже) три. Ёще раз вспомните любую картинку (может быть, свои фотографии с отпуска) и вы поймете, что в нашем обычном понимании на картинке таких координат две: x и y (ширина и высота, их официально принято называть s и t). Чуть позже мы вернёмся к вопросу о других «размерностях».
 +
 +
Установить работу с текстурами логичнее всего при инициализации OpenGL. Пример инициализации текстур:
 +
----
 +
<source lang="cpp-qt">
 +
void Scene3D::initializeGL() // инициализация
 +
{
 +
  // другие настройки ...
 +
 +
  // ...
 +
  genTextures(); // создать текстуры
 +
  glEnable(GL_TEXTURE_2D); // установить режим двумерных текстур
 +
 +
  // ...
 +
  getTextureArray(); // определить массив текстурных координат вершин
 +
 
 +
  // ...
 +
  glEnableClientState(GL_TEXTURE_COORD_ARRAY); // активизация массива текстурных координат
 +
}
 +
</source>
 +
----
 +
Первой функцией 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, аналогично как, например, идентифицируется дисплейный список. Это число есть имя текстурного объекта. После того, как создан текстурный объект, его нужно связать с его состоянием. Состояние текстурного объекта включает в себя само изображение, которое используется как текстура, и то, как оно выводится. Пример создания текстур:
 +
----
 +
<source lang="cpp-qt">
 +
//...
 +
 +
// декларируем массив текстурных объектов (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
 +
  image1=QGLWidget::convertToGLFormat(image1); // конвертируем изображение в формат для работы с OpenGL
 +
  image2=QGLWidget::convertToGLFormat(image2); // конвертируем изображение в формат для работы с OpenGL 
 +
 +
  // создание имён для текстурных объектов
 +
  glGenTextures(2, textureID); // создаём два имени и записываем их в массив
 +
 +
  // создаём и связываем текстурные объекты с состоянием текстуры
 +
  // 1-ый текстурный объект
 +
  glBindTexture(GL_TEXTURE_2D, textureID[0]); // создаём и связываем 1-ый текстурный объект с последующим состоянием текстуры 
 +
  glTexImage2D(GL_TEXTURE_2D, 0, 3, (GLsizei)image1.width(), (GLsizei)image1.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image1.bits()); // связываем текстурный объект с изображением
 +
 +
  // 2-ой текстурный объект
 +
  glBindTexture(GL_TEXTURE_2D, textureID[1]); // создаём и связываем 2-ой текстурный объект с последующим состоянием текстуры 
 +
  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); // задана линейная фильтрация вдали
 +
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // при фильтрации игнорируются тексели, выходящие за границу текстуры для s координаты
 +
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // при фильтрации игнорируются тексели, выходящие за границу текстуры для t координаты
 +
  glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); // цвет текселя полностью замещает цвет фрагмента фигуры
 +
}
 +
</source>
 +
----
 +
В примере создано два текстурных объекта (или просто две текстуры), которые используют изображения picture1.jpg и picture2.jpg из папки textures. "../" в начале адреса означает подняться на одну директорию выше. В системах Windows папка 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) цвета вычисляются умножением цветов текстуры и фрагмента фигуры. Для удаления текстурных объектов используйте glDeleteTextures(2, textureID). Распространённая ошибка: при пересоздании сцены создавать текстурные объекты заново, но не удалять предыдущие; это приведёт к утечке памяти.
 +
 +
Qt может самостоятельно создавать и связывать текстуры, без использования glGenTextures() и явного использования glBindTexture(). Пример:
 +
----
 +
<source lang="cpp-qt">
 +
GLuint id_1; // имя текстуры
 +
 +
void Scene3D::genTexture() // функция genTexture() класса Scene3D, создаёт текстуры
 +
{
 +
  // загрузка изображения
 +
  QImage image1; // создаём объект класса QImage (изображение)
 +
  image1.load("../textures/picture1.jpg"); // загружаем изображение (кратное степени 2) в переменную image1
 +
  image1=QGLWidget::convertToGLFormat(image1); // конвертируем изображение в формат для работы с OpenGL
 +
 +
  // создаём текстуру и связываем её с последующим состоянием
 +
  id_1 = bindTexture(image1, GL_TEXTURE_2D, GL_RGBA, QGLContext::DefaultBindOption);
 +
  glTexImage2D(GL_TEXTURE_2D, 0, 3, (GLsizei)image1.width(), (GLsizei)image1.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image1.bits());
 +
 +
  // далее параметры текстурного объекта
 +
}
 +
</source>
 +
----
 +
Функция bindTexture() класса QGLContext создаёт, связывает и возвращает уникальный номер текстуры. При этом установка текстуры как текущей выполняется как и раньше с помощью glBindTexture(GL_TEXTURE_2D, id_1). Флаг QGLContext::DefaultBindOption уже включает в себя, например, линейную фильтрацию. Текстурный объект id_1 можно также удалить с помощью Qt, используя deleteTexture(id_1).
 +
 +
===Наложение текстур===
 +
 +
Для каждой вершины фигуры задаются текстурные координаты, которые соответствуют точке на самой текстуре. Текстурные координаты полностью определяют, как произойдёт наложение текстуры. Важная особенность, что текстурные координаты нормированы на единицу, т.е. текстурные координаты изменяются от 0 до 1. Крайняя левая нижняя координата текстуры равна (0,0); крайняя правая верхняя — (1,1). Очень наглядно продемонстрировать следующую иллюстрацию, которая всё говорит сама за себя: наложение текстуры на треугольник.
 +
 +
<center>[[Файл:Lecture2demo.jpg]]</center>
 +
 +
Как видно из иллюстрации, текстурные координаты (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() наложение делается следующим образом:
 +
----
 +
<source lang="cpp-qt">
 +
// определены координаты вершин в пространстве:
 +
// 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()
 +
</source>
 +
----
 +
Команды glBegin()/glEnd() сейчас устарели; вместо них используют массивы вершин. Хотя если требуется что-то построить, чтобы определить вид геометрии объектов, можно сделать это через glBegin()/glEnd(), а затем перенести фигуры на массивы. При работе с массивами вершин нужно определить массивы текстурных координат. Массивы вершин и текстурных координат даже удобно определить вместе в одной функции. Пример:
 +
----
 +
<source lang="cpp-qt">
 +
// продекларированы массивы:
 +
// массив вершин: 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,
 +
  // т.е. число треугольников умноженное на число вершин треугольника
 +
}
 +
</source>
 +
----
 +
Вернёмся к одномерным и трёхмерным текстурам. В случае одномерной текстуры её координаты имеют одно измерение. Такие текстуры можно, например, накладывать на линии. Как правило, непонимание вызывают трёхмерные текстуры, которые имеют три измерения. Текстурные координаты можно представить в виде точки в кубе; каждая из трёх текстурных координат лежит в диапазоне [0,1], в итоге и получается куб. Сама трёхмерная текстура представляет собой набор двумерных текстур, следующих друг за другом. То же самое, если бы трёхмерный массив представить набором двумерных массивов. Тексели в этом случае тоже становятся трёхмерными.
 +
 +
===Пример: наложение текстуры на шар===
 +
 +
<center>[[Файл:Lecture2texture.jpg]]</center>
 +
 +
Вот отличный пример: наложение текстуры на шар. Поверхность шара — это классический пример искривлённой поверхности. Нарисованная текстура тоже должна учитывать искривление так, чтобы можно было без особых затруднений вычислить текстурные координаты. К тому же поверхность шара замкнутая, поэтому нужно уточнить, как замыкать текстуру на такой поверхности. Сейчас разберём, как наложить текстуру поверхности Земли на шар.
 +
 +
<center>[[Файл:Lecture2map.png‎]] [[Файл:Lecture2karkas.jpg‎]]</center>
 +
 +
Текстура размером 512x256 позаимствована из CD, распространяющегося вместе с книгой: Евченко А.И. «OpenGL и DirectX. Программирование графики. Для профессионалов», и преобразована в формат .png. Прежде всего, нужно разобраться с геометрией фигуры и проекцией текстуры. В данном случае свяжем текстурную координату x с координатой вершины «фи» (угол фи), а текстурную координату y с координатой вершины «тэта» (угол тэта). Углы тэта и фи есть просто вторая и третья координаты в сферической системе координат; а первая координата для всех вершин одинакова и равна радиусу сферы (шара) R. Посмотрите на рисунки, на которых показано, как связать между собой текстурные координаты и координаты вершин в пространстве.

Версия 09:39, 6 августа 2011

Во 2-ой части продолжается рассмотрение работы с OpenGL на Qt 4. Читатель познакомится с настройкой контекста OpenGL, созданием анимации и простым наложением текстур. Заодно в рамках «чистого» OpenGL будет рассмотрена интерактивная графика с помощью режима выбора (выбираем объекты на экране для дальнейшей манипуляции с ними), в рамках «чистого» Qt будет продемонстрировано создание главного окна, меню (menu bar) и объяснён механизм сигналов и слотов.

Список частей:


Содержание

Введение

Вопросы, связанные с инициализацией OpenGL, с настройкой окна виджета и выводом изображения были рассмотрены [url=http://www.wiki.crossplatform.ru/index.php/%D0%A0%D0%B0%D0%B1%D0%BE%D1%82%D0%B0_%D1%81_OpenGL_%D0%BD%D0%B0_Qt_4]в 1-ой статье[/url]. Также там была нарисована простая трёхмерная фигура с использованием массивов вершин. Во второй части хотелось бы добавить вопросы, связанные с дополнительной настройкой контекста OpenGL, анимацией и наложением текстур. Всё отличие от многократного изложения темы наложения текстур сводится к загрузке изображения средствами Qt. Анимация сводится к работе с таймером в Qt. Но хотелось бы дать столько информации читателю, сколько уже достаточно, чтобы ввести в курс дела и чтобы, отталкиваясь от неё, читатель смог продолжить изучение интересующих вопросов более подробно. Мы будем использовать собственный класс Scene3D, введённый в [url=http://www.wiki.crossplatform.ru/index.php/%D0%A0%D0%B0%D0%B1%D0%BE%D1%82%D0%B0_%D1%81_OpenGL_%D0%BD%D0%B0_Qt_4]1-ой статье[/url]. Стиль изложения будет таким, как и прежде: сначала примеры (куски кода), потом обобщающая всё сказанное программа. Рассказать обо всём невозможно, зато вполне возможно предоставить начальные знания, отталкиваясь от которых, можно продолжить совершенствовать свои навыки. Итак, начнём!

Контекст OpenGL

Контекст, или графический контекст, является состоянием OpenGL. Можно сказать, что контекст — это режим работы OpenGL. По причине платформонезависимости OpenGL часть функций для работы с контекстом предоставляется API оконной системы. Эта часть функций обеспечивает работу с буфером кадров (то, куда выводится изображение). О работе с контекстом пойдёт речь в этом разделе.

Классы QGLFormat и QGLContext

В модуле QtOpenGL определены классы QGLFormat (формат — настройки вывода изображения) и QGLContext (контекст — режим работы OpenGL). С помощью QGLFormat можно задать настройки контекста (формат контекста) и устанавливать их в контекст с помощью QGLContext (установить формат в контекст). Например, использование простой или двойной буферизации при выводе изображения, использование буфера глубины, использование состояния RGBA и т.д. являются контекстом оконной системы — режимом работы с буфером кадров. В общем случае класс QGLContext отвечает за инкапсуляцию настроек контекста.

Настройка контекста

Объект класса QGLContext создаётся автоматически конструктором класса QGLWidget с настройками по умолчанию. Давайте посмотрим, какие настройки по умолчанию задаются автоматически в контекст:


Scene3D::Scene3D(QWidget *parent=0) : QGLWidget(parent) // конструктор класса Scene3D
{ 
   // формат определяется по умолчанию:
   // (+) Double buffer -- двойная буферизация (два буфера: задний и передний): установлена
   // (+) Depth buffer -- буфер глубины: установлен
   // (+) RGBA mode -- состояние RGBA: установлено
   // (+) Stencil buffer -- буфер трафарета: установлен
   // (+) Direct rendering -- прямой вывод изображения: установлен
   // (-) Alpha channel -- альфа-канал (прозрачность): не установлен
   // (-) Accumulator buffer -- буфер накопления: не установлен
   // (-) Stereo buffers -- стерео-буферы (изображения для левого и правого глаза): не установлены
   // (-) Multisample buffers -- мультивыборка (сглаживание): не установлена 
   // (-) Overlay -- наложение: не установлено
   // (-) Plane -- порядок наложения: не установлен 
 
   // автоматически создаётся контекст с таким форматом 
}

Можно при необходимости определить свой формат по умолчанию. Отметим, что все настройки по умолчанию являются наиболее распространёнными и поддерживаются большинством реализаций OpenGL. Например, мультивыборку (сглаживание), простую буферизацию и другие настройки могут не поддерживать встроенные видеоадаптеры. Использование Qt немного отличается от библиотеки GLUT. Например, при двойной буферизации (double buffer) необязательно вызывать команду переключения заднего (невидимого, закадрового) и переднего (видимого, выводимого на экран) буферов. Вы также можете не использовать команду glFlush() (обработать очередь команд), соответствующую завершению команд. Всё это произойдёт автоматически после выполнения функции paintGL(). Но сама функция переключения буферов (для «ручного» управления) есть swapBuffers() класса QGLWidget. Она эквивалентна функции glutSwapBuffers() библиотеки GLUT. Вот как можно установить простую буферизацию (только один буфер кадров, а не два, как при двойной буферизации):


// ...
 
Scene3D::Scene3D(QWidget *parent=0) : QGLWidget(parent) // конструктор класса Scene3D
// Scene3D::Scene3D(QWidget *parent=0) : QGLWidget(QGLFormat(QGL::SingleBuffer/*| QGL::DepthBuffer | QGL::Rgba*/), parent) 
{
   QGLFormat frmt; // создать формат по умолчанию   
   frmt.setDoubleBuffer(false); // задать простую буферизацию
   setFormat(frmt); // установить формат в контекст
 
   // setFormat(QGLFormat(QGL::SingleBuffer)); // можно и так сразу сделать
 
   // проверяем: выводим информацию на консоль
   qDebug() << frmt.doubleBuffer();
 
   // автоматически создаётся контекст с указанными настройками
   // если настроек не было, то создаётся контекст по умолчанию
}
 
// ...

Создать формат и установить его в контекст можно уже при наследовании от QGLWidget(). Так можно сделать благодаря тому, что класс QGLWidget имеет несколько конструкторов. Совокупность константных значений (флагов) QGL::SingleBuffer (простая буферизация), QGL::DepthBuffer (буфер глубины) и QGL::Rgba (режим RGBA) определяют формат, остальные настройки берутся по умолчанию, затем формат передаётся в контекст. Все флаги опций формата указаны в перечислении QGL::FormatOption или флагах QGL::FormatOptions. Читайте о них в Qt Assistant. Далее мы явно создаём формат frmt и устанавливаем простую буферизацию с помощью setDoubleBuffer(false). Затем устанавливаем формат в контекст с помощью setFormat() класса QGLContext. Задать настройки в контекст через setFormat() можно и сразу без явного создания формата так: setFormat(QGLFormat(QGL::SingleBuffer)).

В примере мы используем функцию вывода в консоль qDebug(). Её удобно использовать при работе в интегрированной среде разработки QtCreator, которую можно свободно скачать на официальном сайте Qt (http://qt.nokia.com/downloads). При работе с QtCreator информация выводится во встроенную консоль приложения. В нашем случае в консоль будет выведено false. Как вы догадались, функция doubleBuffer() возвращает состояние двойной буферизации (типа bool), которое выключено. Выключение двойной буферизации означает включение простой буферизации. Изображение будет заметно мигать при обновлении, так как задана простая буферизация, т.е. используется только один буфер кадров. Для проверки qDebug() используйте простой пример:


qDebug() << "Test";

Формат должен быть установлен в контекст только в конструкторе виджета. В противном случае результат будет непредсказуемым. Часто нужно проверить, поддерживается ли конкретная настройка. Пример для проверки мультивыборки:


Scene3D::Scene3D(QWidget *parent=0) : QGLWidget(parent) // конструктор класса Scene3D
{
   if (!format().sampleBuffers()) // мультивыборка не поддерживается
      QMessageBox::warning(0, "Problem", "Multisampling does not supported"); // сообщение-предупреждение
   else // мультивыборка поддерживается
      QMessageBox::information(0, "Ok", "Multisampling is supported"); // сообщение-информация
}

Мультивыборка (т.е. множественная выборка) сглаживает изображения за счёт смешения цвета пикселя с его окружением. Здесь использованы функции format() класса QGLContext и sampleBuffers() класса QGLFormat. Для вывода информации использован класс сообщений QMessageBox, который входит в модуль QtGui.

Версии OpenGL

Прежде, чем начать работу с конкретной версией OpenGL, важно узнать, поддерживается ли она вашей аппаратной платформой, в частном случае драйвером графической карты, или на платформе пользователя приложением. Вкратце скажем, что в OpenGL имеет место так называемый механизм расширений. Производители видеокарт не ждут выхода новой версии OpenGL и выпускают свои собственные расширения (как бы дополнения) — набор новых функий и возможностей OpenGL, поддерживаемый их графическими картами. Если расширение окажется очень полезным для дальнейшего развития OpenGL и пройдёт все тестирования, то у него есть шанс оказаться в ядре следующей версии OpenGL и поддерживаться графическими картами других производителей (подобные вопросы решает консорциум по OpenGL, в который входят крупнейшие корпорации). Разберём на примере, как получить информацию о поддерживаемой версии и расширениях:


// ...
 
#include <QIODevice> // подключаем класс ввода-вывода
 
// ...
void Scene3D::initializeGL() // инициализация
{
   const GLubyte* str = glGetString(GL_VERSION); // возвращает указатель на строку с информацией в памяти  
 
   // выводим информацию в файл
   QFile f("info.txt"); // создать текстовый файл info.txt
   f.open(QIODevice::WriteOnly); // открыть файл только для записи
   QTextStream out(&f); // создать текстовый поток и связать его с файлом
   for(int i=0; i<256; i++)
      out << (char) *(str+i); //вывести в поток
   f.close(); // закрыть файл
 
//...
}

Для определения версии мы используем функцию glGetString() с параметром GL_VERSION, которая возвращает указатель на начало строки информации в памяти. В примере продемонстрирован вывод данных в текстовый файл с помощью Qt, который очень похож на классический способ вывода в C++ с помощью оператора cout (и cin для ввода). В системе Windows XP файл по умолчанию будет создан в той же папке, что и исполняемый файл; в системе openSUSE 11.2 файл info.txt создаётся по умолчанию в "Домашней папке" в "Документах". Важное замечание: glGetString() нужно использовать только после установки контекста OpenGL. В файл info.txt у меня на системе Windows XP выведено:


2.0.6120 WinXP Release

Запись означает: используется драйвер графической карты, поддерживающий реализацию OpenGL 2.0; модификация драйвера 6120; драйвер под Windows XP. Подставляя вместо параметра GL_VERSION параметр GL_VENDOR, получим поставщика реализации (у меня "ATI Technologies Inc."). Параметр GL_RENDERER укажет торговую марку (у меня "Radeon X1600 Series x86/SSE2"). Наконец, glGetString() с параметром GL_EXTENSIONS выдаст через пробел все поддерживаемые драйвером расширения (список довольно большой, поэтому не буду его приводить). При параметре GL_VERSION в Linux, скорее всего, будет выведено название Mesa с номером версии OpenGL. Mesa — это открытая неофициальная реализация OpenGL, на которой основаны аппаратные драйверы под Linux с открытым исходным кодом.

Дополнительно и Qt 4 даёт возможность определить версии OpenGL, поддерживаемые платформой, с помощью функции QGLFormat::openGLVersionFlags() и перечисления QGLFormat::OpenGLVersionFlag или флагов QGLFormat::OpenGLVersionFlags класса QGLFormat. Флаги принимают константные значения в шестнадцатиричной системе исчисления, о чём говорит префикс 0x (cм. Qt Assistant). Версия Qt 4.7.1 распознаёт реализации до OpenGL 4.0 включительно. Функция QGLFormat::openGLVersionFlags() возвращает сумму флагов поддерживаемых версий OpenGL. Например, если QGLFormat::openGLVersionFlags() определяет значение 0x3f (это есть сумма 0x01+0x02+0x04+0x08+0x10+0x20), то это означает поддержку драйвером видеокарты версий OpenGL: 1.1, 1.2, 1.3, 1.4, 1.5 и 2.0. Другой пример: 0x1f будет соответствовать версиям от 1.1 до 1.5. Значения функции и флагов удобно сравнивать с помощью оператора побитового «И» (&) благодаря удобной кодировке самих флагов, представленной в виде смещения ненулевого разряда (единицы в двоичной системе исчисления). Пример:


#include <QtGui/QApplication> // подключаем класс QApplication
#include <QtGui/QMessageBox>  // подключаем класс QMessageBox
#include "scene3D.h" // заголовочный файл, в котором определён класс Scene3D
 
int main(int argc, char** argv) 
{ 
   QApplication a(argc, argv); // создаём приложение 
 
   if (!(QGLFormat::openGLVersionFlags() & QGLFormat::OpenGL_Version_2_0)) // проверяем совместимость с версией OpenGL 2.0
   {
      // если версия OpenGL 2.0 не поддерживается платформой, 
      // то выводим критическое сообщение и завершаем работу приложения
      QMessageBox::critical(0, "Message", "Your platform does not support OpenGL 2.0"); 
      return -1; // завершение приложения
   }
 
   Scene3D s; // создаём виджет класса Scene3D
   s.setWindowTitle("example"); // название окна   
   s.resize(500, 500); // размеры (nWidth, nHeight) окна  
   s.show(); // изобразить виджет
   // s.showFullScreen(); // изобразить виджет на полный экран
   // s.showMaximized(); // изобразить виджет развёрнутым на весь экран 
 
   return a.exec();
}

В случае, если проверяемая версия OpenGL 2.0 не поддерживается платформой пользователя, будет выдано критическое сообщение (виджет) QMessageBox::critical() с соответствующим текстом, после чего приложение завершит работу.

Важно знать, что операционные системы поддерживают не все версии OpenGL. Например, WGL API (или Wiggle — реализация OpenGL в cистеме Windows) ограничивается только версией 1.1 и в этом плане уступает GLX и AGL. В любом случае «доподдержку» остальных версий берёт на себя драйвер графической карты — об этом позаботились производители видеокарт. Для того, чтобы использовать возможности, не реализованные в системных API, необходимо подключить заголовочный файл glext.h, который можно найти на официальной странице OpenGL (http://www.opengl.org/registry/). Этот файл на текущий момент времени содержит макросы (макроопределения) и прототипы функций всех реализаций OpenGL от 1.2 до 4.1 и расширений OpenGL. Сами же функции определены в драйвере и многие из них выполняются аппаратно (в кремниевой схеме графической карты).

Анимация

Смена изображения за некоторый промежуток времени называется анимацией. В OpenGL анимация сопровождается изменением буфера кадров (то, куда выводится изображение). Работа с анимацией сводится к работе с таймером, благодаря которому изображение будет обновляться (изменяться и заново выводиться в буфер кадров) через некоторый интервал времени. Работу с таймером обеспечивает API библиотеки, которую вы используете.

Класс QTimer

В Qt есть два альтернативных способа работы с таймером: 1) низкоуровневые возможности с помощью QObject::startTimer() и QObject::timerEvent(); 2) высокоуровневые возможности с помощью класса QTimer. Мы разберём второй способ, так как он предоставляет разработчикам больше возможностей с использованием механизма сигналов и слотов. Суть механизма сигналов и слотов заключается в том, что сигналы и слоты связываются между собой. Генерация сигнала приводит к выполнению слота. При работе с таймером сигнал будет генерироваться (эмитироваться) через определённый интервал времени. Схема работы с таймером будет такая: 1) создать объект таймера; 2) связать сигналы, генерируемые таймером, со слотом, изменяющим изображение; 3) задать интервал таймера в миллисекундах и запустить его.

Пример из двух файлов scene3D.h и scene3D.cpp:

scene3D.h


#ifndef SCENE3D_H 
#define SCENE3D_H
 
#include <QGLWidget> // подключаем класс QGLWidget
 
class Scene3D : public QGLWidget // класс Scene3D наследует встроенный класс QGLWidget
{ 
   Q_OBJECT // макрос, который нужно использовать при работе с сигналами и слотами
 
   private:      
      GLfloat xRot; // углы поворотов модельно-видовой матрицы
      GLfloat yRot;  
      GLfloat zRot;
      //...
 
   protected:
      //...
 
   private slots:
      void change(); // слот выполняет изменение углов и обновление изображения
 
   public: 
      //...
}; 
#endif

scene3D.cpp


#include <QtGui>     // подключаем модуль QtGui
#include <math.h>    // подключаем математическую библиотеку
#include "scene3D.h" // подключаем заголовочный файл scene3D.h
 
//...
 
Scene3D::Scene3D(QWidget* parent/*= 0*/) : QGLWidget(parent) // конструктор класса Scene3D
{  
   //...
 
   QTimer *timer = new QTimer; // создаём таймер
   connect(timer, SIGNAL(timeout()), this, SLOT(change())); // связываем сигналы, генерируемые таймером, со слотом
   timer->start(30); // запускаем таймер с интервалом 30 миллисекунд
}
 
void Scene3D::paintGL() // рисование
{ 
   //...
 
   glRotatef(xRot, 1.0f, 0.0f, 0.0f); // поворот вокруг оси X
   glRotatef(yRot, 0.0f, 1.0f, 0.0f); // поворот вокруг оси Y
   glRotatef(zRot, 0.0f, 0.0f, 1.0f); // поворот вокруг оси Z
 
   //...
}  
 
//...
 
void Scene3D::change() // слот изменяет углы наблюдения
{
   // изменяем углы поворотов
   xRot +=1.0f;
   yRot +=1.0f;
   zRot -=1.0f;
 
   if ((xRot>360)||(xRot<-360)) xRot=0.0f;
   if ((yRot>360)||(yRot<-360)) yRot=0.0f;
   if ((zRot>360)||(zRot<-360)) zRot=0.0f;
 
   updateGL(); // обновляем изображение, вызываем paintGL()
}
 
//...

Таймер + сигналы и слоты

Обратите внимание на макрос Q_OBJECT, который стоит вначале определения класса. Именно по этому макросу MOC (Meta Object Compiler — метаобъектный компилятор) определит, что в этом классе используется механизм сигналов и слотов и запишет соотвествующую информацию в специальный файл при препроцессорной обработке. Механизм сигналов и слотов может связывать между собой разные классы так, что объект одного класса может создавать сигнал, а объект другого класса получать этот сигнал и реагировать на него. В качестве слота класса Scene3D выступает слот change(), который будет изменять значения углов наблюдения (модельно-видовой матрицы) и обновлять изображение. В конструкторе класса Scene3D мы создаём динамический объект класса QTimer. Затем связываем объекты классов QTimer и нашего класса Scene3D (используем указатель this) с помощью функции connect() класса QObject. Сигнал-функция timeout() будет создавать сигналы таймера через определённый интервал времени. Наш объект this будет получать эти сигналы и реагировать на них вызовом слота-функции change(). Наконец, мы запускаем таймер с помощью start() с интервалом 30 миллисекунд. Обычно погрешность измерения времени операционными системами составляет 1 миллисекунду. Если вы ничего не задали в start() или задали 0, то таймер будет установлен на минимально возможный интервал. Если интервал таймера был уже установлен заранее для данного объекта класса QTimer, то функция start() запускает таймер с этим интервалом. Дополнительную информацию смотрите в Qt Assistant.

Текстуры

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

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

Для того, чтобы использовать текстуры, вам прежде всего потребуется установить состояние 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
   image1=QGLWidget::convertToGLFormat(image1); // конвертируем изображение в формат для работы с OpenGL
   image2=QGLWidget::convertToGLFormat(image2); // конвертируем изображение в формат для работы с OpenGL   
 
   // создание имён для текстурных объектов
   glGenTextures(2, textureID); // создаём два имени и записываем их в массив
 
   // создаём и связываем текстурные объекты с состоянием текстуры
   // 1-ый текстурный объект
   glBindTexture(GL_TEXTURE_2D, textureID[0]); // создаём и связываем 1-ый текстурный объект с последующим состоянием текстуры   
   glTexImage2D(GL_TEXTURE_2D, 0, 3, (GLsizei)image1.width(), (GLsizei)image1.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image1.bits()); // связываем текстурный объект с изображением 
 
   // 2-ой текстурный объект
   glBindTexture(GL_TEXTURE_2D, textureID[1]); // создаём и связываем 2-ой текстурный объект с последующим состоянием текстуры   
   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); // задана линейная фильтрация вдали
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // при фильтрации игнорируются тексели, выходящие за границу текстуры для s координаты
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // при фильтрации игнорируются тексели, выходящие за границу текстуры для t координаты
   glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); // цвет текселя полностью замещает цвет фрагмента фигуры
}

В примере создано два текстурных объекта (или просто две текстуры), которые используют изображения picture1.jpg и picture2.jpg из папки textures. "../" в начале адреса означает подняться на одну директорию выше. В системах Windows папка 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) цвета вычисляются умножением цветов текстуры и фрагмента фигуры. Для удаления текстурных объектов используйте glDeleteTextures(2, textureID). Распространённая ошибка: при пересоздании сцены создавать текстурные объекты заново, но не удалять предыдущие; это приведёт к утечке памяти.

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


GLuint id_1; // имя текстуры
 
void Scene3D::genTexture() // функция genTexture() класса Scene3D, создаёт текстуры
{ 
   // загрузка изображения
   QImage image1; // создаём объект класса QImage (изображение)
   image1.load("../textures/picture1.jpg"); // загружаем изображение (кратное степени 2) в переменную image1
   image1=QGLWidget::convertToGLFormat(image1); // конвертируем изображение в формат для работы с OpenGL
 
   // создаём текстуру и связываем её с последующим состоянием
   id_1 = bindTexture(image1, GL_TEXTURE_2D, GL_RGBA, QGLContext::DefaultBindOption);
   glTexImage2D(GL_TEXTURE_2D, 0, 3, (GLsizei)image1.width(), (GLsizei)image1.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, image1.bits());
 
   // далее параметры текстурного объекта 
}

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

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

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

Файл:Lecture2demo.jpg

Как видно из иллюстрации, текстурные координаты (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], в итоге и получается куб. Сама трёхмерная текстура представляет собой набор двумерных текстур, следующих друг за другом. То же самое, если бы трёхмерный массив представить набором двумерных массивов. Тексели в этом случае тоже становятся трёхмерными.

Пример: наложение текстуры на шар

Файл:Lecture2texture.jpg

Вот отличный пример: наложение текстуры на шар. Поверхность шара — это классический пример искривлённой поверхности. Нарисованная текстура тоже должна учитывать искривление так, чтобы можно было без особых затруднений вычислить текстурные координаты. К тому же поверхность шара замкнутая, поэтому нужно уточнить, как замыкать текстуру на такой поверхности. Сейчас разберём, как наложить текстуру поверхности Земли на шар.

Файл:Lecture2map.png‎ Файл:Lecture2karkas.jpg‎

Текстура размером 512x256 позаимствована из CD, распространяющегося вместе с книгой: Евченко А.И. «OpenGL и DirectX. Программирование графики. Для профессионалов», и преобразована в формат .png. Прежде всего, нужно разобраться с геометрией фигуры и проекцией текстуры. В данном случае свяжем текстурную координату x с координатой вершины «фи» (угол фи), а текстурную координату y с координатой вершины «тэта» (угол тэта). Углы тэта и фи есть просто вторая и третья координаты в сферической системе координат; а первая координата для всех вершин одинакова и равна радиусу сферы (шара) R. Посмотрите на рисунки, на которых показано, как связать между собой текстурные координаты и координаты вершин в пространстве.