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

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

(Различия между версиями)
Перейти к: навигация, поиск
Registr (Обсуждение | вклад)
(Новая страница: «Во 2-ой части продолжается рассмотрение работы с OpenGL на Qt 4. Читатель познакомится с настро…»)
Следующая правка →

Версия 09:15, 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.