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

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

(Различия между версиями)
Перейти к: навигация, поиск
(Рисуем два интерактивных октаэдра)
(Рисуем два интерактивных октаэдра)
Строка 1157: Строка 1157:
}
}
-
void Scene3D::stopTimer() // слот - остановить таймер
+
void Scene3D::stopTmr() // слот - остановить таймер
{
{
   timer->stop();
   timer->stop();
}
}
-
void Scene3D::startTimer() // слот - запустить таймер
+
void Scene3D::startTmr() // слот - запустить таймер
{
{
   timer->start();
   timer->start();

Версия 16:18, 2 октября 2011

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

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


Содержание

Введение

Вопросы, связанные с инициализацией рендеринга OpenGL, с настройкой окна виджета и выводом изображения были рассмотрены в 1-ой статье. Также там была нарисована простая трёхмерная фигура с использованием массивов вершин. Во второй части хотелось бы добавить вопросы, связанные с дополнительной настройкой контекста OpenGL, анимацией и наложением текстур. Может показаться, что всё отличие от многократного изложения темы наложения текстур сводится к загрузке изображения средствами Qt, но это не совсем так: Qt имеет собственные методы для работы с текстурами. В свою очередь, анимация сводится к работе с таймером в Qt. Мне хотелось бы дать столько информации читателю, сколько уже достаточно, чтобы ввести в курс дела и чтобы, отталкиваясь от неё, читатель смог продолжить изучение интересующих вопросов более подробно. Мы будем использовать собственный класс Scene3D, введённый в 1-ой статье. Стиль изложения будет таким, как и прежде: сначала примеры (куски кода), потом обобщающая всё сказанное программа. Рассказать обо всём невозможно, зато вполне возможно предоставить начальные знания, отталкиваясь от которых, можно продолжить совершенствовать свои навыки. Итак, начнём!

Контекст 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 в том плане, что за счёт Qt во многом облегчается работа c OpenGL. Например, при двойной буферизации (double buffer) необязательно вызывать команду переключения заднего (невидимого, закадрового) и переднего (видимого, выводимого на экран) буферов. Вы также можете не использовать команду glFlush() (обработать очередь команд), соответствующую завершению команд. Всё это произойдёт автоматически после выполнения функции paintGL(), а точнее в теле функции glDraw() (класса QGLWidget). Cама функция переключения буферов есть swapBuffers() класса QGLWidget. Она эквивалентна функции glutSwapBuffers() библиотеки GLUT. Схема работы в Qt будет следующая:

glDraw()-->{..., paintGL(), ?swapBuffers(), ?glFlush()}

«Ручное» управление переключением буферов устанавливается с помощью метода setAutoBufferSwap() (класса QGLWidget) с параметром false; в этом случае вам нужно будет самостоятельно переключать буферы в paintGL().

Установить простую буферизацию (только один буфер кадров, а не два, как при двойной буферизации) в контекст можно следующим образом:


// ...
 
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";

Формат может быть установлен в контекст в конструкторе виджета, либо действуя на объект виджета через setFormat() (но только не в самом классе виджета!). Так можно менять контекст OpenGL для виджета в процессе выполнения программы. Пример:


// ...
 
scene1 = new Scene3D; // объект виджета класса Scene3D
 
// ...
 
QGLFormat frmt; // создать формат по умолчанию
frmt.setDoubleBuffer(false); // задать простую буферизацию
 
scene1->setFormat(frmt); // установить формат в контекст виджета
 
//...

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


// ...
 
#include <QtGui/QMessageBox> // подключаем класс сообщений QMessageBox
 
// ...
 
scene1 = new Scene3D; // объект виджета класса Scene3D
 
if (scene1->format().sampleBuffers()) // проверяем, поддерживается ли мультивыборка 
   QMessageBox::information(0, "Ok", "Multisampling is supported"); // сообщение-информация
else // мультивыборка не поддерживается
   QMessageBox::warning(0, "Problem", "Multisampling does not 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.2 и расширений OpenGL. Сами же функции определены в драйвере и многие из них выполняются аппаратно (в кремниевой схеме графической карты).

Анимация

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

Класс QTimer

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

QTimer + сигналы и слоты

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

Про механизм сигналов и слотов следует указать, что можно создавать свои собственные сигналы и их эмитировать; можно соединять сигналы с сигналами; высылку сигналов можно блокировать; сигналы и слоты могут содержать параметры, но, разумеется, одинакового типа; сигналы и слоты можно не только соединять, но и разъединять. При уничтожении объекта все сигнально-слотовые связи с ним разъединяются автоматически. Недостаток механизма сигналов и слотов состоит в том, он работает медленнее, чем механизм функций обратного вызова, и это может быть заметным на «очень медленных» компьютерах. В связи с эти разберем другой дополнительный, низкоуровневый, но более быстрый по выполнению метод. На более-менее современных компьютерах вряд ли вы заметите различие в скорости выполнения.

Другой метод: событие таймера

Этот альтернативный метод не задействует класс QTimer и использует функции startTimer() и timeEvent() класса QObject. Функция startTimer() запускает таймер и возвращает id (идентификатор, т.е. имя) таймера, а функция timeEvent() выполняет событие таймера. Пример:


// ...
 
class Scene3D : public QGLWidget // класс Scene3D наследует встроенный класс QGLWidget
{ 
   private:      
      //...
 
   protected:
      void timerEvent(QTimerEvent *event); // обработка события таймера
      //...
 
   public: 
      //...
}; 
 
//...
 
int id_timer; // идентификатор таймера
 
Scene3D::Scene3D(QWidget* parent) : QGLWidget(parent) // конструктор класса Scene3D
{
   // ...
 
   id_timer=startTimer(10); // устанавливаем таймер id_timer и запускаем его
}
 
void Scene3D::timerEvent(QTimerEvent *event) // событие таймера
{
   // изменения
 
   updateGL(); // обновление изображения
}
 
void Scene3D::stopTmr() // прекратить анимацию
{
   killTimer(id_timer); // уничтожить таймер id_timer
}
 
void Scene3D::startTmr() // запустить анимацию заново
{
   id_timer=startTimer(10); // запустить таймер
}
 
// ...

Функция killTimer() класса QObject уничтожает таймер с указанным в ней id.

Текстуры

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

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

Для того, чтобы использовать текстуры, вам прежде всего потребуется установить состояние 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) цвета вычисляются умножением цветов текстуры и фрагмента фигуры.

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


// ...
 
GLuint textureID[2]; // размер равен двум, в программе будет всего два текстурных объекта
 
// ...
void Scene3D::genTexture() // функция genTexture() класса Scene3D, создаёт текстуры
{
   // создаём, связываем, загружаем, возвращаем уникальный номер:
   textureID[0]=bindTexture(QPixmap(QString("../textures/picture1.jpg")), GL_TEXTURE_2D);
   // далее параметры текстурного объекта
   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); // цвет текселя полностью замещает цвет фрагмента фигуры 
 
   // создаём, связываем, загружаем, возвращаем уникальный номер:
   textureID[1]=bindTexture(QPixmap(QString("../textures/picture2.jpg")), GL_TEXTURE_2D);
   // далее параметры текстурного объекта 
   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); // цвет текселя полностью замещает цвет фрагмента фигуры
}

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

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

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

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

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


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

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

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

Файл:Lecture2texture.jpg

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

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

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

Файл:Lecture2demo1.jpg‎ Файл:Lecture2demo2.jpg

Немного поразмыслив, мы определяем, что текстурные координаты равны:


x=phi/(2*pi); // x=s, pi=3.141593
y=(pi-theta)/pi; // y=t

Но сразу же возникают два важных вопроса:

1) Какие текстурные координаты дать полюсам: северному и южному? Ведь, вообще говоря, вся полоска верхних пикселей текстуры относится к одной вершине: северному полюсу. Та же самая ситуация и с южным полюсом.

2) Как замкнуть текстуру между её правой и левой границами? Ведь одна вершина может иметь только одну текстурную координату.

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

Файл:Lecture2demo3.jpg

Вот возможный вариант, как процедурно определить массивы вершин, текстурных координат и индексов вершин для рассматриваемой задачи:


//...
 
// глобальные переменные
const GLfloat pi=3.141593, k=pi/180; 
const GLuint np=36; // число частей, на которое делится полуокружность
const GLfloat step=pi/np; // шаг изменения углов
 
//...
 
QVector<GLfloat> vecVertices; // вектор вершин
QVector<GLfloat> vecTextures; // вектор текстурных координат
QVector<GLuint> vecIndices; // вектор индексов вершин
 
//...
 
void Scene3D::getVerTexArrays() // определить массив вершин и массив текстурных координат
{
   GLfloat R=0.75f; // радиус сферы
   GLfloat phi, theta; // углы фи и тэта
 
   // двойной цикл по углам
   for (GLuint i=0; i < 2*np+1; i++)
   {
      phi=i*step; // изменение угла фи 
 
      for (GLuint j=0; j < np+1; j++)
      {
         theta=j*step; // изменение угла тэта
 
         if (j==0) // вершины "северный полюс"
         {
            if (i<2*np)
            {
               vecVertices.push_back(0.0f); // пространственная x-координата вершины (добавить в конец вектора)
               vecVertices.push_back(0.0f); // пространственная y-координата вершины
               vecVertices.push_back(R);    // пространственная z-координата вершины
 
               vecTextures.push_back((phi+step/2)/(2*pi)); // текстурная x-координата вершины
               vecTextures.push_back(1.0f);                // текстурная y-координата вершины
            }
         }
         else
         {
            if (j<np) // вершины между северными и южными полюсами
            {
               if (i<2*np)
               {
                  vecVertices.push_back(R*sin(theta)*cos(phi));
                  vecVertices.push_back(R*sin(theta)*sin(phi));
                  vecVertices.push_back(R*cos(theta));
 
                  vecTextures.push_back(phi/(2*pi));
               }
               else
               {
                  vecVertices.push_back(R*sin(theta));
                  vecVertices.push_back(0.0f);
                  vecVertices.push_back(R*cos(theta));
 
                  vecTextures.push_back(1.0f);
               }
               vecTextures.push_back((pi-theta)/pi);
            }
            else // вершины "южный полюс"
            {
               if (i<2*np)
               {
                  vecVertices.push_back(0.0f);
                  vecVertices.push_back(0.0f);
                  vecVertices.push_back(-R);
 
                  vecTextures.push_back((phi+step/2)/(2*pi));
                  vecTextures.push_back(0.0f);
               }
            }
         }
      }
   }
}
 
void Scene3D::getIndexArray() // определить массив индексов
{
   for (GLuint i=0; i < 2*np; i++)
   {
      for (GLuint j=0; j < np; j++)
      {
         if (j==0)
         {
            vecIndices.push_back(i*(np+1));
            vecIndices.push_back(i*(np+1)+1);
            if (i<(2*np-1))
               vecIndices.push_back((i+1)*(np+1)+1);
            else
               vecIndices.push_back((i+1)*(np+1));
         }
         else
         {
            if (j<(np-1))
            {
               if (i<(2*np-1))
               {
                  vecIndices.push_back(i*(np+1)+j);
                  vecIndices.push_back((i+1)*(np+1)+(j+1));
                  vecIndices.push_back((i+1)*(np+1)+j);
 
                  vecIndices.push_back(i*(np+1)+j);
                  vecIndices.push_back(i*(np+1)+(j+1));
                  vecIndices.push_back((i+1)*(np+1)+(j+1));
               }
               else
               {
                  vecIndices.push_back(i*(np+1)+j);
                  vecIndices.push_back((i+1)*(np+1)+j);
                  vecIndices.push_back((i+1)*(np+1)+(j-1));
 
                  vecIndices.push_back(i*(np+1)+j);
                  vecIndices.push_back(i*(np+1)+(j+1));
                  vecIndices.push_back((i+1)*(np+1)+j);
               }
            }
            else
            {
 
               vecIndices.push_back((i+1)*(np+1)-1);
               if (i<(2*np-1))
                  vecIndices.push_back((i+2)*(np+1)-2);
               else
                  vecIndices.push_back((i+2)*(np+1)-3);
               vecIndices.push_back((i+1)*(np+1)-2);
            }
         }
      }
   }
}
 
//...
 
void Scene3D::drawFigure() // построить фигуру
{
   // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // изобразить фигуру в виде каркаса
   // glColor4f(0.00f, 0.00f, 1.00f, 1.0f); // синий цвет
 
   glVertexPointer(3, GL_FLOAT, 0, vecVertices.begin()); // указываем, откуда нужно извлечь данные о массиве вершин
   glTexCoordPointer(2, GL_FLOAT, 0, vecTextures.begin()); // указываем, откуда нужно извлечь данные о массиве текстурных координат
   glDrawElements(GL_TRIANGLES, vecIndices.size(), GL_UNSIGNED_INT, vecIndices.begin()); // строим поверхности
   // glPointSize(3.0f); // размер точки примерно в пикселях
   // glDrawArrays(GL_POINTS, 0, vecVertices.size()/3); // изобразить массив в виде точек
}

В примере мы воспользовались контейнерами-векторами QVector класса QVector (входит в состав модуля QtCore) вместо обычных массивов. QVector полностью аналогичен контейнеру-вектору vector из STL (Standard Template Library — стандартная библиотека шаблонов). Удобство контейнеров заключается в том, что изначально мы можем не указывать число его элементов и добавлять их в контейнер по мере необходимости. Функции для QVector совпадают по названию с аналогичными функциями для vector. Например, push_back() — добавить в конец контейнера; begin() — вернуть указатель на начало контейнера; size() — вернуть число элементов контейнера; clear() — очистить контейнер и т.д.

Закомментируйте glTexCoordPointer() и glEnableClientState(GL_TEXTURE_COORD_ARRAY), или отключите последнюю с помощью glDisableClientState(GL_TEXTURE_COORD_ARRAY), и раскомментируйте последовательные команды glPolygonMode() и glColor4f() и увидите каркас, на который наложена текстура. При вычислениях массивов вершин иногда бывает полезно посмотреть на сами вершины, не заморачиваясь с массивом индексов. Для этого используйте glDrawArrays() вместо glDrawElements().

Рисуем два интерактивных октаэдра

Файл:Lecture2.jpg


Теперь мы рассмотрим листинг полной программы с анимацией и текстурами. Для этого мы создадим главное окно и меню (menu bar), чтобы управлять некоторыми процессами на сцене. Для большего эффекта мы сделаем интерактивную графику с помощью режима выбора OpenGL, благодаря которому сможем выбирать мышью трёхмерные объекты на сцене. В качестве трёхмерных объектов мы возьмём два октаэдра, наложим на каждый из них по разной текстуре и заставим их вращаться и перемещаться. (Для общего сведения, октаэдр — это такая фигура, которая имеет 6 вершин и строится из 8 (равносторонних) треугольников.) С помощью опций меню мы сможем менять текстуры октаэдров и останавливать или запускать таймер. Дополнительно мышью мы сможем выбирать октаэдры на сцене, чтобы остановить или возобновить их движения, т.е. поставлена такая задача: щёлкаем мышкой по фигуре и фигура должна остановиться; щёлкаем ещё раз и она опять должна двигаться и т.д. Для этого используется режим выбора и буфер выбора OpenGL. Программа тестировалась на Windows XP + Qt 4.7.1 + OpenGL 2.0 и openSUSE 11.2 + Qt 4.5 + OpenGL 1.5. Листинг состоит из пяти файлов: scene3D.h, scene3D.cpp, mainwindow.h, mainwindow.cpp и main.cpp.

scene3D.h


#ifndef SCENE3D_H 
#define SCENE3D_H
 
#include <QGLWidget> // подключаем класс QGLWidget
 
class Scene3D : public QGLWidget // класс Scene3D наследует встроенный класс QGLWidget
{ 
   Q_OBJECT
 
   private:   
      GLfloat ratio; // отношение высоты окна виджета к его ширине
 
      // для первой фигуры
      GLfloat xRot1; // угол поворота вокруг оси X
      GLfloat yRot1; // угол поворота вокруг оси Y
      GLfloat zRot1; // угол поворота вокруг оси Z
      GLfloat zTra1; // величина трансляции по оси Z
 
      // для второй фигуры
      GLfloat xRot2; // угол поворота вокруг оси X
      GLfloat yRot2; // угол поворота вокруг оси Y
      GLfloat zRot2; // угол поворота вокруг оси Z
      GLfloat zTra2; // величина трансляции по оси Z
 
      QTimer *timer; // декларируем таймер
 
      void getVerTexArrays(); // определить массив вершин
      void getIndexArray();   // определить массив индексов вершин
      void genTextures();     // создать текстуры  
      void drawFigure();      // построить фигуру
 
      void selectFigures(QPoint mp); // выбрать фигуру      
 
   protected:
      void initializeGL();                     // метод для проведения инициализаций, связанных с OpenGL
      void resizeGL(int nWidth, int nHeight);  // метод вызывается при изменении размеров окна виджета
      void paintGL();                          // метод, чтобы заново перерисовать содержимое виджета
      void mousePressEvent(QMouseEvent* pe);   // метод обработки события мыши при нажатии клавиши мыши
 
   private slots: // слоты
      void change(); // изменение углов поворота и величины трансляции
      void changeTex(); // поменять текстуры местами
      void stopTmr(); // остановить таймер
      void startTmr(); // запустить таймер
 
   public: 
      Scene3D(QWidget* parent = 0); // конструктор класса
      ~Scene3D();                   // деструктор
}; 
#endif

В заголовочном файле scene3D.h мы декларируем наш класс Scene3D. В функции selectFigures() будет использоваться режим выбора. Параметром этой функции является объект типа QPoint, который хранит в себе x и y координату экрана. В классе мы декларируем четыре слота, которые будут выполняться при эмитировании связанных с ними сигналов.

scene3D.cpp


#include <QtGui>      // подключаем модуль QtGui
#include <math.h>     // подключаем математическую библиотеку
#include "scene3D.h"  // подключаем заголовочный файл scene3D.h
#include "glext.h"    // подключаем заголовочный файл glext.h
 
///* в версии 1.2 */
//#ifndef GL_CLAMP_TO_EDGE
//#define GL_CLAMP_TO_EDGE 0x812F
//#endif
 
///* в версии 1.3 */
//#ifndef GL_MULTISAMPLE
//#define GL_MULTISAMPLE  0x809D
//#endif
 
const GLfloat pi=3.141593, k=pi/180; // глобальная переменная
 
GLint viewport[4]; // декларируем матрицу поля просмотра
 
GLfloat VertexArray[6][3];  // декларируем массив вершин
GLfloat TextureArray[6][2]; // декларируем массив текстурных координат
GLuint IndexArray[8][3];    // декларируем массив индексов вершин
GLuint textureID[2];        // декларируем массив текстурных объектов (id текстур)
 
GLint signs[2]={1, 1}; // массив знаков +1 или -1 (понадобятся при трансляции)
bool motionParameters[2]={1, 1}; // массив параметров, определяющих движение
int textureParameters[2]={0, 1}; // массив индексов массива текстурных ID
 
Scene3D::Scene3D(QWidget* parent/*= 0*/) : QGLWidget(QGLFormat(QGL::SampleBuffers), parent) // конструктор класса Scene3D
{  
   // начальные значения
   xRot1=-90.0f; yRot1=0.0f; zRot1=0.0f; zTra1=0.0f;
   xRot2=-90.0f; yRot2=0.0f; zRot2=0.0f; zTra2=0.0f;
 
   timer = new QTimer; // создаём таймер
   connect(timer, SIGNAL(timeout()), this, SLOT(change())); // связываем сигналы и слоты
   timer->start(30); // запускаем таймер
}
 
Scene3D::~Scene3D() // деструктор
{
 
}
 
void Scene3D::initializeGL() // инициализация
{
   qglClearColor(Qt::black); // цвет для очистки буфера изображения - здесь просто фон окна
   glEnable(GL_DEPTH_TEST);  // устанавливает режим проверки глубины пикселей
   glEnable(GL_CULL_FACE);   // устанавливается режим, когда строятся только внешние поверхности 
   glEnable(GL_TEXTURE_2D);  // установить режим двумерных текстур
   glEnable(GL_MULTISAMPLE); // включаем мультивыборку
 
   getVerTexArrays(); // определить массив вершин и текстурных координат
   getIndexArray();   // определить массив индексов вершин
   genTextures();     // создать текстуры
 
   glEnableClientState(GL_VERTEX_ARRAY); // активизация массива вершин
   glEnableClientState(GL_TEXTURE_COORD_ARRAY); // активизация массива текстурных координат
}
 
void Scene3D::resizeGL(int nWidth, int nHeight) // окно виджета
{ 
   glMatrixMode(GL_PROJECTION); // устанавливает текущей проекционную матрицу
   glLoadIdentity();            // присваивает проекционной матрице единичную матрицу
 
   ratio=(GLfloat)nHeight/(GLfloat)nWidth; // отношение высоты окна виджета к его ширине
 
   // мировое окно
   if (nWidth>=nHeight)
      glOrtho(-2.0/ratio, 2.0/ratio, -2.0, 2.0, -10.0, 10.0); // параметры видимости ортогональной проекции
   else
      glOrtho(-2.0, 2.0, -2.0*ratio, 2.0*ratio, -10.0, 10.0); // параметры видимости ортогональной проекции
 
   // поле просмотра
   glViewport(0, 0, (GLint)nWidth, (GLint)nHeight);
 
   glGetIntegerv(GL_VIEWPORT, viewport); // извлечь матрицу поля просмотра в viewport
}
 
void Scene3D::paintGL() // рисование
{ 
   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // очистка буфера изображения и глубины
 
   glMatrixMode(GL_MODELVIEW); // устанавливает положение и ориентацию матрицы моделирования
   glLoadIdentity();           // загружает единичную матрица моделирования
 
   glPushMatrix(); // поместить матрицу моделирования в стек матриц
      glTranslatef(-1.0f/ratio, zTra1, 0.0f); // трансляция
      glRotatef(xRot1, 1.0f, 0.0f, 0.0f); // поворот вокруг оси X
      glRotatef(yRot1, 0.0f, 1.0f, 0.0f); // поворот вокруг оси Y
      glRotatef(zRot1, 0.0f, 0.0f, 1.0f); // поворот вокруг оси Z
      glBindTexture(GL_TEXTURE_2D, textureID[textureParameters[0]]); // выбираем текстуру для наложения
      drawFigure(); // нарисовать фигуру
   glPopMatrix(); // извлечь матрицу моделирования из стека матриц
 
   glPushMatrix(); // поместить в стек матрицу моделирования
      glTranslatef(1.0f/ratio, zTra2, 0.0f); // трансляция
      glRotatef(xRot2, 1.0f, 0.0f, 0.0f); // поворот вокруг оси X
      glRotatef(yRot2, 0.0f, 1.0f, 0.0f); // поворот вокруг оси Y
      glRotatef(zRot2, 0.0f, 0.0f, 1.0f); // поворот вокруг оси Z
      glBindTexture(GL_TEXTURE_2D, textureID[textureParameters[1]]); // выбираем текстуру для наложения
      drawFigure(); // нарисовать фигуру
   glPopMatrix(); // взять из стека матрицу моделирования
}
 
void Scene3D::mousePressEvent(QMouseEvent* pe) // нажатие клавиши мыши
{
   // при нажатии пользователем кнопки мыши переменной mousePosition будет
   QPoint mousePosition = pe->pos(); // присвоена координата указателя мыши
 
   this->selectFigures(mousePosition); // выбрать фигуру
}  
 
void Scene3D::genTextures() // создаём текстуры
{
   // создаём, связываем, загружаем, возвращаем уникальный номер:
   textureID[0]=bindTexture(QPixmap(QString("../textures/picture1.jpg")), GL_TEXTURE_2D);
   // далее параметры текстурного объекта
   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); // цвет текселя полностью замещает цвет фрагмента фигуры 
 
   // создаём, связываем, загружаем, возвращаем уникальный номер:
   textureID[1]=bindTexture(QPixmap(QString("../textures/picture2.jpg")), GL_TEXTURE_2D);
   // далее параметры текстурного объекта 
   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); // цвет текселя полностью замещает цвет фрагмента фигуры
}
 
void Scene3D::getVerTexArrays() // определить массив вершин и текстурных координат
{
   GLfloat R=1.0f; // радиус сферы
   GLfloat alpha=pi/2;
 
   // вычисляем точки
   //0 точка
   VertexArray[0][0]=0.0f; // x
   VertexArray[0][1]=0.0f; // y
   VertexArray[0][2]=R;    // z
 
   TextureArray[0][0]=0.5f; // x (s)
   TextureArray[0][1]=1.0f; // y (t)
 
   //1 точка
   VertexArray[1][0]=R*sin(alpha)*sin(0);
   VertexArray[1][1]=R*sin(alpha)*cos(0);
   VertexArray[1][2]=R*cos(alpha);
 
   TextureArray[1][0]=1.0f;
   TextureArray[1][1]=0.0f;
 
   //2 точка
   VertexArray[2][0]=R*sin(alpha)*sin(pi/2);
   VertexArray[2][1]=R*sin(alpha)*cos(pi/2);
   VertexArray[2][2]=R*cos(alpha);
 
   TextureArray[2][0]=0.0f;
   TextureArray[2][1]=0.0f;
 
   //3 точка
   VertexArray[3][0]=R*sin(alpha)*sin(pi);
   VertexArray[3][1]=R*sin(alpha)*cos(pi);
   VertexArray[3][2]=R*cos(alpha);
 
   TextureArray[3][0]=1.0f;
   TextureArray[3][1]=0.0f;
 
   //4 точка
   VertexArray[4][0]=R*sin(alpha)*sin(3*pi/2);
   VertexArray[4][1]=R*sin(alpha)*cos(3*pi/2);
   VertexArray[4][2]=R*cos(alpha);
 
   TextureArray[4][0]=0.0f;
   TextureArray[4][1]=0.0f;
 
   //5 точка
   VertexArray[5][0]=0.0f;
   VertexArray[5][1]=0.0f;
   VertexArray[5][2]=-R;
 
   TextureArray[5][0]=0.5f;
   TextureArray[5][1]=1.0f;
}
 
void Scene3D::getIndexArray() // определить массив индексов
{
   // 0 треугольник
   IndexArray[0][0]=0; // индекс (номер) 1-ой вершины
   IndexArray[0][1]=2; // индекс (номер) 2-ой вершины
   IndexArray[0][2]=1; // индекс (номер) 3-ей вершины
 
   // 1 треугольник
   IndexArray[1][0]=0;
   IndexArray[1][1]=3;
   IndexArray[1][2]=2;
 
   // 2 треугольник
   IndexArray[2][0]=0;
   IndexArray[2][1]=4;
   IndexArray[2][2]=3;
 
   // 3 треугольник
   IndexArray[3][0]=0;
   IndexArray[3][1]=1;
   IndexArray[3][2]=4;
 
   // 4 треугольник
   IndexArray[4][0]=5;
   IndexArray[4][1]=1;
   IndexArray[4][2]=2;
 
   // 5 треугольник
   IndexArray[5][0]=5;
   IndexArray[5][1]=2;
   IndexArray[5][2]=3;
 
   // 6 треугольник
   IndexArray[6][0]=5;
   IndexArray[6][1]=3;
   IndexArray[6][2]=4;
 
   // 7 треугольник
   IndexArray[7][0]=5;
   IndexArray[7][1]=4;
   IndexArray[7][2]=1;
}
 
void Scene3D::drawFigure() // построить фигуру
{
   glVertexPointer(3, GL_FLOAT, 0, VertexArray); // указываем, откуда нужно извлечь данные о массиве вершин
   glTexCoordPointer(2, GL_FLOAT, 0, TextureArray); // указываем, откуда нужно извлечь данные о массиве текстурных координат
   glDrawElements(GL_TRIANGLES, 24, GL_UNSIGNED_INT, IndexArray); // используя массивы вершин и индексов, строим поверхности
}
 
void Scene3D::selectFigures(QPoint mp) // выбрать фигуру
{
   GLuint selectBuffer[4]; // буфер выбора (буфер совпадений)
   GLint hits; // счетчик соответствий
   glSelectBuffer(4, selectBuffer); // использовать указанный буфер выбора
 
   glMatrixMode(GL_PROJECTION); // матрица проекции стала активной
   glPushMatrix(); // поместить текущую матрицу в стек матриц
   glRenderMode(GL_SELECT); // переход в режим выбора
   glLoadIdentity(); // загрузить единичную матрицу
 
   // новый объём под указателем мыши
   gluPickMatrix((GLdouble)mp.x(), (GLdouble)(viewport[3]-mp.y()), 1.0, 1.0, viewport);
   // мировое окно
   if (width()>=height())
      glOrtho(-2.0/ratio, 2.0/ratio, -2.0, 2.0, -10.0, 10.0); // параметры видимости ортогональной проекции
   else
      glOrtho(-2.0, 2.0, -2.0*ratio, 2.0*ratio, -10.0, 10.0); // параметры видимости ортогональной проекции
 
   glMatrixMode(GL_MODELVIEW); // модельно-видовая матрица стала активной
   glLoadIdentity();           // загружается единичная матрица моделирования
 
   glInitNames(); // инициализируется и очищается стек имён
   glPushName(0); // в стек имён помещается значение 0 (обязательно должен храниться хотя бы один элемент)
 
   glPushMatrix(); // поместить матрицу моделирования в стек матриц
      glTranslatef(-1.0f/ratio, zTra1, 0.0f); // трансляция
      glRotatef(xRot1, 1.0f, 0.0f, 0.0f); // поворот вокруг оси X
      glRotatef(yRot1, 0.0f, 1.0f, 0.0f); // поворот вокруг оси Y
      glRotatef(zRot1, 0.0f, 0.0f, 1.0f); // поворот вокруг оси Z
      glLoadName(1); // загрузить имя на вершину стека имён
      drawFigure(); // нарисовать фигуру
   glPopMatrix(); // извлечь матрицу моделирования из стека матриц
 
   glPushMatrix(); // поместить в стек матрицу моделирования
      glTranslatef(1.0f/ratio, zTra2, 0.0f); // трансляция
      glRotatef(xRot2, 1.0f, 0.0f, 0.0f); // поворот вокруг оси X
      glRotatef(yRot2, 0.0f, 1.0f, 0.0f); // поворот вокруг оси Y
      glRotatef(zRot2, 0.0f, 0.0f, 1.0f); // поворот вокруг оси Z
      glLoadName(2); // загрузить имя на вершину стека имён
      drawFigure(); // нарисовать фигуру
   glPopMatrix(); // взять из стека матрицу моделирования
 
   glPopName(); // вытащить имя из стека имён
 
   hits=glRenderMode(GL_RENDER); // число совпадений и переход в режим рисования
 
   if (hits>0) // есть совпадания и нет ошибок
   {
      int figureName=selectBuffer[3]; // имя фигуры
 
      if (motionParameters[figureName-1]) // изменение параметров движения
         motionParameters[figureName-1]=0; // углы и величина трансляции не изменяется
      else
         motionParameters[figureName-1]=1; // углы и величина трансляция изменяются
   }
 
   glMatrixMode(GL_PROJECTION); // матрица проекции стала активной
   glPopMatrix(); // извлечь матрицу из стека матриц
}
 
void Scene3D::change() // слот - изменение углов поворота и величины трансляции
{
   if (motionParameters[0]) // изменение для первой фигуры
   {
      xRot1 -=1.0f;
      yRot1 -=1.0f;
      zRot1 +=1.0f;
 
      if ((xRot1>360)||(xRot1<-360)) xRot1=0.0f;
      if ((yRot1>360)||(yRot1<-360)) yRot1=0.0f;
      if ((zRot1>360)||(zRot1<-360)) zRot1=0.0f;
 
      if (abs(zTra1)>0.5f) signs[0] *=-1;
      zTra1 -=signs[0]*0.01f;
   }
 
   if (motionParameters[1]) // изменение для второй фигуры
   {
      xRot2 +=1.0f;
      yRot2 +=1.0f;
      zRot2 -=1.0f;
 
      if ((xRot2>360)||(xRot2<-360)) xRot2=0.0f;
      if ((yRot2>360)||(yRot2<-360)) yRot2=0.0f;
      if ((zRot2>360)||(zRot2<-360)) zRot2=0.0f;
 
      if (abs(zTra2)>0.5f) signs[1] *=-1;
      zTra2 +=signs[1]*0.01f;
   }
 
   updateGL(); // обновить изображение
}
 
void Scene3D::changeTex() // слот - поменять текстуры местами
{
   if (textureParameters[0]==0)
   {
      textureParameters[0]=1;
      textureParameters[1]=0;
   }
   else
   {
      textureParameters[0]=0;
      textureParameters[1]=1;
   }
 
   updateGL(); // обновить изображение
}
 
void Scene3D::stopTmr() // слот - остановить таймер
{
   timer->stop();
}
 
void Scene3D::startTmr() // слот - запустить таймер
{
   timer->start();
}

После подключения файла scene3D.h мы подключаем файл glext.h, который содержит все макроопределения после версии OpenGL 1.1. Так как мы используем только одно макроопределение из 1.2 и одно из 1.3, можно было бы не подключать glext.h, а определить макросы самостоятельно, как показано далее в закомментированной части. Откуда мы взяли эти макроопределения? Ответ очевиден: из файла glext.h. Далее идут глобальные переменные и массивы. В массив viewport будет загружаться матрица поля просмотра. Массив signs содержит числа +1 или -1, т.е. положительные или отрицательные знаки числа; это пригодится при изменении величины трансляции. В массиве motionParameters число 1 будет означать, что нужно изменять углы и величины трансляции; а число 0, что не нужно. Массив textureParameters содержит индексы массива textureID. Массивы signs, motionParameters, textureParameters и textureID содержат два элемента: 0-ой элемент для первой фигуры; 1-ый — для второй. В конструкторе Scene3D мы устанавливаем буферы мультивыборки для сглаживания изображения. В конце функции resizeGL() с помощью glGetIntegerv() происходит загрузка матрицы поля просмотра в массив viewport, который будет нужен в режиме выбора для определения наблюдаемого объёма.

Используя стеки матриц

Текущие матрицы можно сохранять и загружать. Загрузка матриц происходит быстрее, чем, если бы сначала устанавливалась единичная матрица, а потом над ней проводились необходимые преобразования. Самый простой способ сохранения и загрузки матрицы заключается в помещении её в стек матриц. Стек матриц как и любой стек работает по принципу «первым зашёл — последним вышел» и «последним зашёл — первым вышел». В стек матриц должно помещаться не более двух матриц. Команды для работы со стеком матриц: glPushMatrix() — поместить в стек матриц, glPopMatrix() — извлечь из стека матриц. В листинге стеки матриц задействованы при рисовании в функции paintGL() и далее в функции selectFigures().

Накладываемые текстуры

В функции genTextures() создаются два текстурных объекта. Под текстуры загружаются изображения picture1.jpg и picture2.jpg размером 256x256:

Файл:Lecture2picture1.jpg Файл:Lecture2picture2.jpg‎

Рисунки под текстуры взяты со свободного ресурса (http://www.cgtextures.com/) и преобразованы под нужный размер. Команды glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE) закомментированы и цвета фрагментов фигуры и фрагментов текстуры будут перемножаться. Но мы не установили цвет, и в этом случае по умолчанию был установлен белый цвет, для которого компоненты равны 1. Поэтому умножение цвета фрагмента текстуры на белый цвет никак не изменяет конечный цвет фрагмента. По умолчанию (режим намотки текстуры) накладываемая текстура будет симметрично отражаться от треугольника к треугольнику.

Используя режим выбора

В функции selectFigures() демонстрируется работа с буфером выбора, с помощью которого можно выбирать графические объекты на сцене. Все примитивы можно объединить в группы (графические объекты), дав им имена в виде целого числа без знака (тип GLuint). В нашем случае такими двумя графическими объектами являются два октаэдра. По умолчанию всегда установлен режим рендера (когда изображение выводится в буфер кадров), поэтому нужно перейти в режим выбора — такой режим визуализации, когда изображение не выводится в буфер кадров. Когда щёлкается клавиша мыши, определяется координата на экране под указателем мыши. Далее около координаты мыши формируется наблюдаемый объём, в котором ищутся графические объекты (т.е. графические объекты, которые пересекают указанный объём). При переходе обратно в режим рендера буфер выбора заполняется записями соответствий. Из буфера выбора затем извлекается вся необходимая информация, в частности, имена графических объектов в наблюдаемом объёме.

Первым делом мы декларируем массив selectBuffer, который и называется буфером выбора. Он должен содержать как минимум четыре элемента. Число hits есть число совпадений (т.е. число записей соответствий); у нас оно станет равным 1 (когда есть одно совпадение) или 0 (когда нет совпадения). В функции glSelectBuffer() указывается, какой массив мы выделяем для буфера выбора. Затем нужно перейти к текущей матрице проекции с помощью glMatrixMode(GL_PROJECTION) и сохранить её в стеке матриц (чтобы после задания нового объёма вернуть из стека матриц нашу старую матрицу обратно). Функция glRenderMode() с параметром GL_SELECT осуществляет переход в режим выбора. Команда glLoadIdentity() загружает единичную матрицу в матрицу проекции. Теперь нам нужно создать новый объём, в котором будут искаться графические объекты; это делается с помощью функции gluPickMatrix(). В функцию gluPickMatrix() нужно передать координаты в окне OpenGL (т.е. координаты мыши в окне), указать размеры нового объёма в виде ширины и высоты (у нас они равны одному пикселю) и также передать матрицу поля просмотра viewport. Мы должны учесть, что координата y окна «перевёрнута» относительно координаты y OpenGL. Поэтому в y-параметр функции gluPickMatrix() нужно вставить: viewport[3]-mp.y(), здесь элемент viewport[3] равен высоте поля просмотра; мы также могли бы написать вместо viewport[3] функцию height() класса QWidget. Затем ещё нужно задать проекцию и мировое окно.

В параметрах функции gluPickMatrix() скрыт ещё один нюанс: для нас важно, где определяется функция mousePressEvent() с методом pos(), а она может определяться и в классе Scene3D (центральный виджет), и в классе главного окна. Дело в том, что под меню сверху экрана выделяется область, равная по высоте примерно 20 пикселям. Например, в системе Windows XP высота под меню равна 20 пикселям, а в системе OpenSuse 11.2 она равна 21 пикселю. Тогда поле просмотра недополучает эти 20 пикселей по высоте, т.е. поле просмотра становится меньше по высоте примерно на 20 пикселей. Если методы, связанные с размером по высоте, pos(), y(), height() используется в главном окне, то они учитывают дополнительный размер меню. Но если они используются в центральном виджете (это наш класс Scene3D), то размеры меню они уже никак учитывать не будут. В обоих случаях отсчёт ведётся как бы «от себя». Поэтому здесь принципиально важно, чтобы функция mousePressEvent() и методы pos(), y(), height() находились все в одном классе, тогда смещение от меню будет либо учитываться автоматически, либо не учитываться вообще. Для их размещения мы выбрали класс Scene3D, это более логично и к тому же требует меньше программного кода.

Следующим шагом будет: нарисовать графические объекты в режиме выбора и дать им имена. Поэтому устанавливается модельно-видовая матрица и рисование схоже тому, как это делается в функции paintGL(), за исключением того, что мы даём фигурам имена, и нас не интересует, какие текстуры накладываются. Для того, чтобы дать фигурам имена, сначала инициализируется и очищается стек имён с помощью glInitNames() и в него помещается хотя бы одно имя с помощью glPushName(). Именно с этой позицией в стеке имён мы и будем работать далее. Заталкивание ещё имён в стек имён нужно при иерархическом отборе, когда нужно установить иерархию выбранного объекта вроде: машина37-->деталь457-->болт1253. Вообще в стек имён можно поместить как минимум 64 имени. Далее команда glLoadName() даёт имя графическом объекту и помещает его на вершину стека имён, т.е. замещает старое имя. В режиме выбора попавшие в наблюдаемый объём фигуры формируют записи соответствий. Вкратце скажем, что каждая запись соответствий содержит 4 элемента: 1) количество имён в стеке имён (у нас равно 1); 2-3) максимальная и минимальная оконные координаты z, которые из диапазона [0,1] масштабируются на диапазон [0, максимальное число без знака]; 4) самый нижний элемент стека имён (т.е. имя графического объекта внизу стека имён). Та же самая процедура проводится для остальных графических объектов.

Наконец, когда покидается режим выбора (переход в режим рендера) буфер выбора заполняется записями соответствий. Для одного совпадения (т.е. одной записи соответствий) элемент буфера выбора selectBuffer[3] есть имя графического объекта, на который щёлкнули мышью. Вам не составит труда самостоятельно разобрать, что происходит дальше в теле функции selectFigures(); происходящее уже не относится к режиму выбора, а связано с реакцией программы на выбранные объекты.

Создаём главное окно и меню

В Qt есть специальный класс QMainWindow (входит в состав модуля QtGui), обеспечивающий работу с главным окном. Главное окно уже имеет готовый каркас: меню (menu bar), панели инструментов (toolbars), доки (dock widgets), центральный виджет (central widget) и строку текущего состояния (status bar). В главном окне обязательно должен быть установлен центральный виджет; остальные компоненты могут и не присутствовать. Хорошее описание главного окна и его элементов приведено в Qt Assistant. Мы ограничимся тем, что в качестве центрального виджета возьмём объект нашего класса Scene3D и для управления им добавим в главное окно меню (menu bar) — объекты класса QMenu, в которые в свою очередь добавим объекты класса QAction.

mainwindow.h


#ifndef MAINWINDOW_H 
#define MAINWINDOW_H 
 
#include <QMainWindow> 
#include "scene3D.h"
 
class MainWindow : public QMainWindow // класс MainWindow как подкласс QMainWindow
{ 
   Q_OBJECT
 
   private:
      Scene3D* scene1; // указатель на объект класса Scene3D
 
      // меню
      QMenu* texture_menu; // указатель на меню, меню текстуры            
      // дейстивие
      QAction* changeTexAct; // поменять текстуры местами
 
      // меню
      QMenu* timer_menu; // меню таймера      
      // действия
      QAction* stopTimAct; // остановить таймер
      QAction* startTimAct; // запустить таймер
 
      void createActions(); // создание действий
      void createMenus(); // создание меню
 
   public:   
      MainWindow(); // конструктор 
}; 
#endif

Qt облегчает работу с виджетами с помощью объектов класса QAction, или действий. Это как бы абстрактные действия, которые связываются со слотами и вставляются в виджеты. В файле mainwindow.h продекларированы функция createActions(), которая создаёт объекты действия, и функция createMenus(), которая создаёт меню.

mainwindow.cpp


#include <QtGui>
#include "mainwindow.h"
#include "scene3D.h"
 
MainWindow::MainWindow() // конструктор
{ 
   scene1 = new Scene3D; // создаю динамический объект класса Scene3D
   setCentralWidget(scene1); // обозначаю scene1 центральным виджетом в главном окне
 
   this->setWindowTitle(tr("lecture2")); // название главного окна
 
   createActions(); // создаю действия
   createMenus(); // создаю меню
}
 
void MainWindow::createActions() // создать объекты действий и связать их со слотами
{
   changeTexAct = new QAction(tr("Change"), this); // создать действие
   connect(changeTexAct, SIGNAL(triggered()), scene1, SLOT(changeTex())); // связать сингалы и слоты
 
   stopTimAct = new QAction(tr("Stop"), this);
   connect(stopTimAct, SIGNAL(triggered()), scene1, SLOT(stopTimer()));
 
   startTimAct = new QAction(tr("Start"), this);
   connect(startTimAct, SIGNAL(triggered()), scene1, SLOT(startTimer()));
}
 
void MainWindow::createMenus() // создать меню
{
   texture_menu = menuBar()->addMenu(tr("Texture")); // добавить группу меню
   texture_menu->addAction(changeTexAct); // добавить в меню действие
 
   timer_menu = menuBar()->addMenu(tr("Animation"));
   timer_menu->addAction(stopTimAct);
   timer_menu->addAction(startTimAct);
}

В конструкторе MainWindow() создаётся объект класса Scene3D и он устанавливается центральным виждетом главного окна с помощью setCentralWidget(). Затем мы задаём название главного окна и создаём действия и меню. В функции createActions() создаются действия, которые связываются со слотами. В качестве сигнала-функции используется triggered(). Она эмитирует сигнал, когда пользователь активирует опцию меню, например, щёлкнет по опции меню. В функции createMenus() создаётся меню. Функция menuBar() создаёт меню для главного окна, а addMenu() добавляет в него конкретную группу меню. Затем функция addAction() добавляет в эту группу меню конкретное действие.

main.cpp


#include <QtGui>
#include "mainwindow.h" 
 
int main(int argc, char** argv) 
{ 
   QApplication app(argc, argv);  // создаётся приложение
 
   MainWindow mainwindow1; // создаётся главное окно   
   mainwindow1.resize(500, 500); // размеры главного окна
   // mainwindow1.show();  // показать главное окно
   // mainwindow1.showFullScreen();  // показать главное окно на весь экран
   mainwindow1.showMaximized(); // показать главное окно максимально развёрнутым 
 
   return app.exec();
}

В главной функции main() создаётся главное окно, устанавливаются его размеры по умолчанию и оно показывается максимально развёрнутым.

Включение изображения в исполняемый файл

Часто бывает удобно включить в исполняемый файл изображения, которые используются в программе. Тогда уже папка с изображениями будет не нужна при запуске приложения. Для этого нужно создать файл ресурсов. В нашем случае файл ресурсов picture.qrc будет иметь вид:

picture.qrc


<RCC> 
<qresource> 
<file>textures/picture1.jpg</file>
<file>textures/picture2.jpg</file>
</qresource>
</RCC>

Этот файл также нужно указать в файле-проекте. Мы приведём тело всего файла-проекта:

lecture2.pro


TEMPLATE = app
TARGET = 
DEPENDPATH += .
INCLUDEPATH += .
RESOURCES = picture.qrc
 
# Input
HEADERS += glext.h scene3D.h mainwindow.h
SOURCES += main.cpp scene3D.cpp mainwindow.cpp
QT += opengl

Изображения после компиляции будут включены в исполняемый файл и его размер заметно увеличится.

Заключение

Мы рассмотрели наиболее распространённые аспекты «классического» OpenGL. В статье был сделан упор на вводный курс информации, чтобы читатель смог создать собственное приложение с трёхмерной графикой и при необходимости самостоятельно изучить интересующие его вопросы. На этом вторая часть статьи заканчивается. Благодарю вас за внимание!

Литература

C++:

  • Аверкин В.П., Бобровский А.И., Веснич В.В., Радушинский В.Ф., Хомоненко А.Д. «Программирование на C++» (очень хорошая книга для начинающих, отличный самоучитель)
  • Глушаков С.В., Коваль А.В., Смирнов С.В. «Язык программирования C++» (хороший справочник)
  • Дейтел Х., Дейтел П. «Как программировать на C++» (ОЧЕНЬ БОЛЬШАЯ книга, в ней много всего полезного)

OpenGL:

  • Райт Р.С.-мл., Липчак Б. «OpenGL. Суперкнига» (отличная книга по OpenGL, отличный самоучитель)
  • Ву М., Девис Т., Нейдер Дж., Шрайнер Д. «OpenGL. Руководство по программированию» (отличный справочник)
  • Уроки NeHe (уроки, ставшие классикой)

Qt:

  • Qt Assistant и Qt Examples and Demos (помощь и много нужных примеров, поставляются вместе с Qt)
  • Бланшет Ж., Саммерфилд М. «QT 4: программирование GUI на C++»
  • Шлее М. «Qt4. Профессиональное программирование на C++»
  • Боровский А. «Qt и ваша видеокарта» (статья, посвященная сегодняшним и будущим технологиям Qt при работе с OpenGL/ES и OpenCL)

Дублирование авторской статьи: http://www.gamedev.ru/code/articles/OpenGL_Qt4_part2

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

(c) registr

Если вы обнаружили ошибки и неточности, то сообщите, пожалуйста, о них в комментариях, либо сообщите по эл. почте: registrcontact@mail.ru.