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

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

Автор:

Контекст OpenGL

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

Классы QGLFormat и QGLContext

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

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

Объект класса 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. Mesa — это открытая неофициальная реализация OpenGL, на которой основаны аппаратные драйверы под Linux с открытым исходным кодом. Для 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); // создаём приложение 
   
   // проверяем совместимость с версией OpenGL 2.0
   if (!(QGLFormat::openGLVersionFlags() & QGLFormat::OpenGL_Version_2_0))
   {
      // если версия OpenGL 2.0 не поддерживается платформой, 
      // то выводим критическое сообщение и завершаем работу приложения
      QMessageBox::critical(0, "Message", "Your platform does not support OpenGL 2.0"); 
      return -1; // завершение приложения
   }

   Scene3D scene1; // создаём виджет класса Scene3D
   scene1.setWindowTitle("example"); // название окна   
   scene1.resize(500, 500); // размеры (nWidth, nHeight) окна  
   scene1.show(); // изобразить виджет
   // scene1.showFullScreen(); // изобразить виджет на полный экран
   // scene1.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. Сами же функции определены в драйвере и многие из них выполняются аппаратно (в кремниевой схеме графической карты).

Обратите внимание, что в этом разделе мы рассмотрели некоторые способы вывода информации.

Анимация

Смена изображения за некоторый промежуток времени называется анимацией. Скорость смены изображения измеряется кадровой частотой, т.е. числом кадров в секунду (fps — frames per second). Видеоанимация — это изменение кадра в буфере кадров (то, куда выводится изображение). Работа с анимацией в 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(this); // создаём объект таймера,
                                     // потомка объекта класса Scene3D
   // связываем сигналы, генерируемые таймером, со слотом:
   connect(timer, SIGNAL(timeout()), this, SLOT(change()));
   timer->start(20); // запускаем таймер с интервалом 20 миллисекунд
}

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 +=0.1f;
   yRot +=0.1f;
   zRot -=0.1f;

   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, потомка объекта класса Scene3D, о чём говорит параметр this. Затем связываем объекты классов QTimer и нашего класса Scene3D (используем указатель this) с помощью функции connect() класса QObject. Cигнал-функция должна быть вставлена в макрос-функцию SIGNAL(), слот-функция — в макрос SLOT(). Сигнал-функция timeout() будет вызывать сигналы таймера через определённый интервал времени. Наш объект this будет получать эти сигналы и реагировать на них вызовом слота-функции change(). Наконец, мы запускаем таймер с помощью start() с интервалом 20 миллисекунд. Обычно погрешность измерения времени операционными системами составляет 1 миллисекунду. Если вы ничего не задали в start() или задали 0, то таймер будет установлен на минимально возможный интервал. Если интервал таймера был уже установлен заранее для данного объекта класса QTimer, то функция start() запускает таймер с этим интервалом. Функция stop() остановит таймер, если это будет нужно, до следующего вызова start(). Дополнительную информацию смотрите в Qt Assistant.

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

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

Этот альтернативный метод не задействует класс QTimer, а значит и не задействует механизм сигналов и слотов, и использует функции startTimer() и timeEvent() класса QObject. Функция startTimer() запускает таймер с указанным в ней интервалом (также в миллисекундах) и возвращает id (идентификатор, т.е. имя) таймера, а функция timeEvent() выполняет событие таймера. В теле виртуальной переопределённой функции 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 
                            // и запускаем его с интервалом 10 миллисекунд
}
 
void Scene3D::timerEvent(QTimerEvent *event) // событие таймера
{
   // изменения
 
   updateGL(); // обновление изображения
}
 
void Scene3D::stopTmr() // прекратить анимацию
{
   killTimer(id_timer); // уничтожить таймер id_timer
}
 
void Scene3D::startTmr() // запустить анимацию заново
{
   id_timer=startTimer(10); // запустить таймер
}
 
// ...
Функция killTimer() класса QObject уничтожает таймер с указанным в ней id. Событие таймера является более точным методом работы с таймером, чем использование сигналов и слотов. Визуально, конечно, это незаметно.

Синхронизация кадров с дисплеем

На первый взгляд может показаться, что чем меньше интервал таймера и чем меньше изменения сцены, тем плавнее будет анимация. В действительности это не так. Уменьшение интервалов не приводит к плавности анимации, т.е. анимация будет выглядеть как бы рваной, ступенчатой, неплавной. Причина этого состоит в том, что работа монитора (дисплея) и работа буфера кадров не согласованы между собой, т.е. не синхронизированы. Мы не будем вдаваться во все подробности (например, в подробности о работе монитора и частоте обновления изображения монитором, и как под это должен подстраиваться вывод в буфер кадров), а лишь покажем как синхронизировать буфер кадров с работой монитора. Для этого в конструктор класса Scene3D нужно добавить всего три строки:

Scene3D::Scene3D(QWidget* parent) : QGLWidget(QGLFormat(QGL::SampleBuffers), parent)
{  
   // ...

   // всего три строки - синхронизация буфера кадров с монитором:
   QGLFormat frmt; // создать формат по умолчанию
   frmt.setSwapInterval(1); // установить синхронизацию в формат
   setFormat(frmt); // установить формат в контекст

   // ...
}
В добавленной первой строке мы создаем формат по умолчанию; во второй — задаем синхронизацию в формат; а в третьей — устанавливаем формат в контекст. Функция setSwapInterval() принадлежит классу QGLFormat, поэтому на нее распространяется все сказанное ранее о работе с форматом и контекстом. В частности, как вы заметили, синхронизация задается и устанавливается в контекст в конструкторе класса Scene3D.

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

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

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

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