Работа с OpenGL на Qt 4 (часть 1)
Материал из Wiki.crossplatform.ru
Это ознакомительная статья, посвящённая программированию 3D-графики OpenGL (Open Graphics Library — открытая графическая библиотека) с помощью кроссплатформенной библиотеки Qt 4 (Q + toolkit: «Q» — префикс классов (с красивым начертанием, по мнению разработчиков); toolkit — инструментарий). Я надеюсь, она окажется полезной для тех, кто впервые решил познакомиться с OpenGL и выбирает практичную и удобную библиотеку GUI (Graphical User Interface — графический интерфейс пользователя).
Список частей:
- Работа с OpenGL на Qt 4 (часть 1)
- Работа с OpenGL на Qt 4 (часть 2)
Содержание |
[править] Введение
Возможности библиотеки Qt далеко выходят за рамки разработки GUI и она считается одной из самых успешных библиотек для разработки кроссплатформенных приложений для различных целей. Конечно же, статья направлена на популяризацию Qt как API (Application Programming Interface — интерфейс прикладного программирования) для создания трёхмерной графики. Поэтому эта статья также окажется полезной для профессиональных программистов OpenGL, незнакомых с возможностями Qt 4, и программистов Qt, не работавших с OpenGL. От читателя потребуется знание языка C++. Изложение будет построено таким образом: сначала мы рассмотрим основы вместе с примерами — кусками кода. И в конце будет написана обобщающая всё сказанное программа.
[править] Модуль QtOpenGL
Библиотека Qt4 имеет специальный модуль для работы с OpenGL — QtOpenGL. Модуль QtOpenGL реализован как платформонезависимая Qt/C++ надстройка (стандартизация внешних обращений) над платформозависимыми GLX (OpenGL в Linux), WGL (OpenGL в Windows) или AGL (OpenGL в MacOS) API. Функциональность модуля QtOpenGL очень схожа с библиотекой GLUT, но при этом вам предоставляются все возможности Qt, которые несвязаны с OpenGL. В модуле QtOpenGL версии Qt 4.7.1 определены следующие классы: QGLBuffer, QGLColormap, QGLContext, QGLFormat, QGLFramebufferObject, QGLFramebufferObjectFormat, QGLPixelBuffer, QGLShader, QGLShaderProgram, QGLWidget, QWSGLWindowSurface.
Классы подключаются стандартным образом:
#include <QGLWidget> // подключаем класс QGLWidget #include <QtOpenGL> // подключаем весь модуль QtOpenGL, он уже содержит класс QGLWidget
[править] Класс QGLWidget
Рассмотрим класс QGLWidget вместе с наиболее важными функциями-членами и слотами-членами этого класса.
Класс:
QGLWidget
Конструктор класса:
QGLWidget::QGLWidget(QWidget* parent = 0) // конструктор класса, создает объект класса
Функции-члены класса (protected):
virtual void QGLWidget::initializeGL() // устанавливает начальные значения контекста рендеринга virtual void QGLWidget::resizeGL(int width, int height) // отвечает за изменение размеров окна виджета virtual void QGLWidget::paintGL() // рисует изображение
Слот-член класса (public):
virtual void QGLWidget::updateGL() // вызывает обновление изображения
Выполнение функций происходит автоматически по следующей схеме:
(создание виджета)--> ... -->glDraw()-->{?glInit()-->initializeGL(), ?resizeGL(), paintGL(), ...}
(изменение размера окна)--> ... -->glDraw()-->{?glInit(), ?resizeGL(), paintGL(), ...}
updateGL()-->glDraw()-->{?glInit(), ?resizeGL(), paintGL(), ...}
Знак вопроса (?) на схеме условно означает, что в этих местах проверяется, нужно ли сейчас вызвать эти функции или нет. Более полную и подробную информацию о классе QGLWidget можно найти в справке Qt Assistant, который всегда поставляется с Qt, а ещё подробнее смотрите в исходных текстах Qt.
Аргументом конструктора класса является указатель на объект класса QWidget. Класс QWidget обеспечивает работу с виджетами (интерфейсными элементами окна), а класс QGLWidget унаследован от QWidget и осуществляет связь виджетов и OpenGL. Запись QWidget *parent=0 означает, что виджет parent по умолчанию будет как бы «главным» окном (или точнее, окном-объектом верхнего уровня в иерархии объектов). Пример:
#include <QGLWidget> // подключаем класс QGLWidget class Scene3D : public QGLWidget // новый класс Scene3D наследует встроенный класс QGLWidget { private: //... protected: /*virtual*/ void initializeGL(); // метод для проведения инициализаций, связанных с OpenGL /*virtual*/ void resizeGL(int nWidth, int nHeight); // метод вызывается при изменении размеров окна /*virtual*/ void paintGL(); // метод, чтобы заново перерисовать содержимое виджета public: Scene3D(QWidget* parent = 0); // конструктор класса Scene3D }; //... Scene3D::Scene3D(QWidget* parent/*= 0*/) : QGLWidget(parent) // конструктор класса Scene3D { // передает дальше указатель parent }
Класс виджета, который будет работать с OpenGL, должен наследоваться от класса QGLWidget. Мы назвали этот класс Scene3D. Объектом класса Scene3D будет виджет, в котором будет рисоваться трёхмерная сцена. Конструктор класса Scene3D сначала вызовит конструктор класса QGLWidget, который в свою очередь вызовит конструктор класса QWidget.
В Qt имеет место так называемая иерархия объектов (класса QObject или его классов-наследников), когда связь между объектами образует древовидную структуру (условно сверху вниз). Эту структуру называют деревом объектов. На вершине иерархии находится объект, от которого идёт вниз связь с другими объектами. Объект на вершине иерархии объектов называется объектом верхнего уровня. Он, являясь (как бы) объектом-родителем, связан с нижележащими объектами, которые являются для него (как бы) объектами-потомками. Эти объекты-потомки могут быть объектами-родителями для следующих нижележащих объектов и т.д. В Qt при уничтожении объекта-родителя все его объекты-потомки уничтожаются автоматически. Такая связь эффективна для управления памятью, когда достаточно удалить только верхний в иерархии объект и все связанные с ним нижележащие объекты удалятся автоматически. Здесь важно заметить, что объекты-потомки должны создаваться динамически (через new), иначе может произойти ошибка, связанная с вызовами деструктров. А использование динамических объектов-потомков исключает эту возможную ошибку. Смысл технологии дерева объектов заключается в том, что объект верхнего уровня создаётся нединамически: обычно это виджет окна или виджет главного окна (это и есть объект верхнего уровня). А остальные GUI-объекты создаются динамически как объекты-потомки этого главного виджета (объекта верхнего уровня). Когда объект главного виджета выйдет за область видимости, то всё дерево объектов удалится из памяти. В этом и заключается вся прелесть работы с деревом объектов — не нужно держать в голове динамические объекты GUI и удалять их через delete в деструкторе. В этой части статьи мы не будем создавать дерево объектов и отложим это до второй части. Здесь в обобщающей программе, которая будет приведена во второй половине этой (первой части) статьи, наш объект класса Scene3D будет объектом верхнего уровня. В конечном итоге в конструктор класса QWidget передастся значение 0, что и будет означать, что наш объект класса Scene3D является объектом верхнего уровня в иерархии объектов.
В конструкторе Scene3D() нужно задать начальные значения, не являющиеся как таковыми командами OpenGL, например, углы поворотов при наблюдении сцены. Определять деструктор в данном случае необязательно; деструктор по умолчанию сам создастся компилятором. Функции initializeGL(), resizeGL(), paintGL() являются виртуальными функциями (подробнее о динамическом полиморфизме можно прочитать, например, в книге: Аверкин В.П., Бобровский А.И., Веснич В.В., Радушинский В.Ф., Хомоненко А.Д. «Программирование на C++»). Следовательно, в классе-наследнике Scene3D обозначать функции как virtual не нужно. Эти три функции изначально имеют полностью пустое тело, которое нужно заполнить командами OpenGL. Здесь нужно отметить, что функции glDraw() и glInit() принадлежат классу QGLWidget, а не к OpenGL (как можно было бы подумать из префикса gl). Функции glDraw() и glInit() не нужны для пользовательской работы; они просто вызывают initializeGL(), resizeGL(), paintGL(), о которых и пойдёт речь далее. Как раз только эти три функци и используются для пользовательской работы с OpenGL: при инициализации настроек рендеринга, при изменении размера окна виджета и при рисовании.
[править] Инициализация контекста рендеринга
Функция initializeGL() вызывается только один раз после создания виджета. Судя по названию, в теле этой функции вы можете провести какую-либо инициализацию, т.е. установить некоторые начальные значения — речь идёт о контексте рендеринга OpenGL. Разумеется, инициализацию контекста рендеринга можно провести где-то в другом месте, но разумнее задать контекст рендеринга именно в этой функции — для этого эта функция и была создана. И, конечно же, можно изменять контекст рендеринга в любом удобном для этого месте. Пример:
/*virtual*/ void Scene3D::initializeGL() // инициализация { qglClearColor(Qt::white); // цвет для очистки буфера изображения - здесь просто фон окна glEnable(GL_DEPTH_TEST); // устанавливает режим проверки глубины пикселей // glShadeModel(GL_FLAT); // отключает режим сглаживания цветов glEnable(GL_CULL_FACE); // устанавливается режим, когда строятся только внешние поверхности }
Обратим внимание на первую команду qglClearColor(Qt::white), которая принадлежит классу QGLWidget. В OpenGL она эквивалентна glClearColor(1.0f, 1.0f, 1.0f, 1.0f), которая устанавливает цвет RGBA для очистки окна. Конечно же, вместо qglClearColor(Qt::white) можно использовать glClearColor(1.0f, 1.0f, 1.0f, 1.0f), и выбор определяется удобством и остаётся за вами. Аргументом функции qglClearColor() является значение типа QColor. Это также встроенный класс Qt, как можно догадаться из приставки Q. Особенность его в том, что он соответствует цветам в OS Windows от 0 до 255. Пример:
#include <QtGui> // подключаем модуль QtGui, он содержит класс QColor //... QColor burmaline(195, 155, 175, 255); // бурмалиновый цвет, последний аргумент прозрачность
В соответствии с правилами обозначения в OpenGL значение 1.0f обрабатывается как тип GLfloat (собственный тип OpenGL). Зачем нужны собственные типы? Дело в том, что различные компиляторы и платформы по-разному распределяют память под стандартные типы и программист должен всегда это держать в уме. Собственные типы OpenGL обрабатываются везде одинаково и освобождают нас от этого обременительного занятия.
Следующая функция glEnable(GL_DEPTH_TEST) является уже функцией OpenGL, что можно понять из приставки gl в отличие от qgl для первой функции. Функция glEnable(GL_DEPTH_TEST) устанавливает режим проверки глубины пикселей объектов (например, для двух последовательно рисующихся треугольников). Зачем это нужно? Дело в том, что одни объекты могут быть ближе к нам (наблюдателям), а другие дальше, а значит, ближние объекты могут закрывать собой дальние. OpenGL никак не проверяет, какой объект ближе, а какой дальше, он просто последовательно рисует объекты. Если последовательно рисуются: объект1, объект2, то даже если объект2 дальше от наблюдателя, он все равно будет закрывать собой объект1, потому что объект2 нарисовался последним. Но как объяснить OpenGL, какой объект ближе, а какой дальше? На помощь нам приходит технология отсечения скрытых (т.е. невидимых) поверхностей с помощью буфера глубины, известного также как z-буфер. Суть этой технологии состоит в том, что каждому пикселю объекта (например, треугольнику) на экране с координатами x и y даётся еще координата z, называемая глубиной и характеризующая расстояние до наблюдателя. Задача сводится к тому, чтобы не рисовать «закрытые» пиксели объектов. Если сохранить глубину пикселей объекта1 в буфер глубины и потом сравнить с глубиной пикселей объекта2, то можно определить, какие пиксели объектов видны на экране, а какие скрыты. Например, рассмотрим такую ситуацию: последовательно рисуются два треугольника. Когда обрабатывается первый треугольник, то значения глубины его пикселей сохраняются в буфер глубины. Затем когда обрабатывается второй треугольник, то глубина уже его пикселей сравнивается со значениями в буфере глубины, и те пиксели, которые ближе к нам, замещают те, которые дальше. Команда glEnable(GL_DEPTH_TEST) включает режим, при котором проводится сравнение глубины пикселей объектов с глубиной, хранящейся в буфере глубины. Также нужно будет сообщить системе об использовании буфера глубины, т.е. нужно подключить сам буфер глубины. Qt автоматически в конструкторе класса QGLWidget по умолчанию задаст такой формат в контекст, т.е. использовать (подключить) буфер глубины. Поэтому буфер глубины можно явно и не подключать, он подключится сам. Использование буфера глубины и сравнение глубины являются эффективным способом удаления (отсечения) невидимых поверхностей.
Закомментированная функция glShadeModel(GL_FLAT) отключает режим сглаживания цветов, который всегда установлен по умолчанию. Если вершины имеют разный цвет, то цвет между ними будет плавно переходить из одного в другой. Отключать режим сглаживания нет необходимости, поэтому функция закомментирована и не используется.
OpenGL использует следующие простые геометрические построения — примитивы: точка, линия, треугольник (чаще всего), четырёхугольник, многоугольник (реже всего). Например, когда рисуется треугольник, рисуются его внешняя и внутренняя поверхности. Но часто бывает так, что внутренние поверхности рисовать ненужно, так как мы их никогда не увидим. Для этого используется команда glEnable(GL_CULL_FACE) и в результате её исполнения рисуются только внешние поверхности. Как можно догадаться, это повышает быстродействие рисования, так как теперь видеокарта рисует изображение в два раза меньше. Какая поверхность внешняя, а какая внутренняя определяется с помощью последовательности её вершин — закручивание по правилу правого винта (обход вершин по часовой стрелке — вектор нормали направлен от вас; против часовой стрелки — вектор нормали на вас).
[править] Настройки окна виджета
Рассмотрим следующую функцию resizeGL(), определенную в Qt. Эта функция вызывается первый раз после initializeGL() и каждый раз автоматически, когда происходит изменение размера окна. Как вы догадываетесь параметры width и height типа int и есть ширина и высота окна виджета в пикселях, которые отсчитывается от самой верхней левой точки виджета. Эти параметры передаются в функцию resizeGL() автоматически при ее вызове. В теле этой функции нужно задавать настройки, связанные с размером виджета, т.е. поле просмотра, мировое окно и вид проекции. Пример:
/*virtual*/ void Scene3D::resizeGL(int nWidth, int nHeight) // окно виджета { glMatrixMode(GL_PROJECTION); // устанавливает текущей проекционную матрицу glLoadIdentity(); // присваивает проекционной матрице единичную матрицу // мировое окно glOrtho(-1.0, 1.0, -1.0, 1.0, -10.0, 1.0); // параметры видимости ортогональной проекции // плоскости отсечения (левая, правая, верхняя, нижняя, передняя, задняя) // glFrustum(-1.0, 1.0, -1.0, 1.0, 1.0, 10.0); // параметры видимости перспективной проекции // плоскости отсечения: (левая, правая, верхняя, нижняя, ближняя, дальняя) // поле просмотра glViewport(0, 0, (GLint)nWidth, (GLint)nHeight); // устанавливает видовое окно с размерами равными окну виджета }
Последовательные команды glMatrixMode(GL_PROJECTION) и glLoadIdentity() загружают единичную матрицу в матрицу проекции. Параметром glMatrixMode() является матрица, над которой будут совершаться все последующие преобразования. Команда glMatrixMode(GL_PROJECTION) как раз означает, что текущей матрицей для преобразований станет матрица проекции. Для матрицы проекции таковыми преобразованиями являются установление видимого объёма и типа проекции. Необходимо знать, что любые преобразования с матрицами не сбрасываются, а аддитивно накапливаются. Поэтому необходимо каждый раз возвращать первоначальные матрицы, в которых не произведено никаких преобразований. С этой цель единичная матрица и загружается в текущую матрицу (проекции), т.к. единичная матрица соответствует матрице без преобразований. Если не отменять преобразования, то следующее преобразование будет совершаться уже относительно предыдущего.
Команда glOrtho() означает, что проекция является ортогональной (также ортографической, ортонормальной), в отличие от перспективной проекции, задающейся командой glFrustum(). В перспективной проекции чем дальше объекты находятся от наблюдателя, тем они рисуются меньше, а в ортогональной нет эффекта удаления. Аргументами функции glOrtho() являются плоскости отсечения: левая, правая, верхняя, нижняя, передняя, задняя. Для функции glFrustum() плоскости отсечения: левая, правая, верхняя, нижняя, ближняя, дальняя; при этом наблюдатель находится в точке (0, 0, 0) и расстояния до ближней и дальней плоскостей должно быть положительным. Обратите внимание, что аргументы функции обозначены не 10.0f, а просто 10.0. По умолчанию такие значения относятся к GLdouble.
Функция glViewport(0, 0, (GLint)nWidth, (GLint)nHeight) определяет поле просмотра (порт просмотра) внутри окна в пикселях экрана и образует прямоугольник с левой нижней точкой (0, 0) и правой верхней точкой (nWidth, nHeight). В этом поле и будет всё рисоваться. Для нас важно, что прямоугольник поля просмотра совпадает с прямоугольником виджета окна. Так как nWidth и nHeight типа int, стоит произвести преобразования к типам GLint.
Часто новички, задавая одинаковые первоначальные значения ширины и высоты, недоумевают почему их изображение растягивается по ширине при развертывании окна на весь экран. Ничего удивительного в этом нет — ширина становится больше по значению, чем высота. И OpenGL автоматически масштабирует всю проекцию сцены так, что она становится вытянутой по координате x экрана. Чуть позже в заключительном листинге обобщающей программы будет показано, как этого избежать и «сохранить квадрат квадратным». Также необходимо помнить, что отсчёт координаты y на экране производиться по-разному: из самой нижней точки вверх в OpenGL и из самой верхней точки вниз в экранных координатах в виджете.
Построение изображения в OpenGL происходит по следующему принципу: (мировые координаты)-->(мировое окно)-->(поле просмотра).
[править] Вывод изображения
Следующая рассматриваемая функция Qt — paintGL() — вызывает рисование сцены и выполняется первый раз после resizeGL() и каждый раз после функции вызова updateGL(). Пример:
/*virtual*/ void Scene3D::paintGL() // рисование { // glClear(GL_COLOR_BUFFER_BIT); // окно виджета очищается текущим цветом очистки glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // очистка буфера изображения и глубины glMatrixMode(GL_MODELVIEW); // устанавливает положение и ориентацию матрице моделирования glLoadIdentity(); // загружает единичную матрицу моделирования // последовательные преобразования 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 glTranslatef(xTra, yTra, zTra); // трансляция glScalef(xSca, ySca, zSca); // масштабирование по осям example_drawAxis(); // рисование осей координат }
Первая закомментированная команда glClear(GL_COLOR_BUFFER_BIT) очищает окно цветом RGBA, который выбран командами qglClearColor(Qt::white) или glClearColor(1.0f, 1.0f, 1.0f, 1.0f) (в данном случае белый цвет). Если в инициализации initializeGL() установлена проверка глубины glEnable(GL_DEPTH_TEST), то можно очистить буфер цвета и глубины вместе glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT).
Команда glMatrixMode(GL_MODELVIEW) делает матрицу моделирования (имеются также переводы модельно-видовая матрица) текущей для последующих преобразований. Далее glLoadIdentity() загружает единичную матрицу в матрицу моделирования, аналогично как это говорилось выше о матрице проекции. Матрица моделирования отвечает за наблюдение сцены; а матрица проекции за проецирование сцены, которое может быть либо ортогональным, либо перспективным.
Далее приведены последовательные преобразования: поворот, трансляция, масштабирование. Значения (GLfloat) xRot, yRot, zRot, xTra, yTra, zTra, xSca, ySca, zSca удобно определить как private данные-члены класса и задать им начальные значения в теле конструктора класса. Как вы догадались, это углы поворота вокруг трёх осей, величины трансляции и масштабирования. Особенность углов поворота в том, что они задаются в градусах, а не в радианах. Функция glRotatef() осуществляет поворот наблюдателя на заданный угол вокруг заданной оси. Соответственно, функции glTranslatef() и glScalef() производят трансляцию сцены и масштабирование. Важно напомнить ещё раз, что любое преобразование изменяет текущие матрицы. И следующее преобразование осуществляется относительно предыдущего. Поэтому операции (поворот->трансляция) и (трансляция->поворот) приводят к разным результатам; операции (поворот1->поворот2) и (поворот2->поворот1) также могут привести к разным конечным преобразованиям. Все эти преобразования в итоге изменяют матрицу моделирования от единичной. Именно поэтому необходимо при перерисовании сцены загружать единичную матрицу, после чего будут осуществляться указанные преобразования над ней. К тому же вы можете загружать матрицы, сделанные «своими руками».
Теперь можно полностью представить, как происходит долгий путь построения изображения в OpenGL: (мировые координаты)--> ... -->(координаты в окне). Конвейер преобразований: (мировые координаты)-->[матрица моделирования]-->(преобразованные координаты)-->[матрица проекции]-->(мировое окно - координаты с отсечением)-->[перспективное преобразование]-->(преобразованные координаты)-->[преобразование поля просмотра]-->(координаты в окне). В конечном итоге изображение попадает в буфер кадров, который управляется операционной системой и хранится в видеопамяти. Этот процесс от начала до конца называется рендерингом. А дисплей «захватывает» кадры из буфера кадров и выводит их на экран.
Нашу последнюю функцию example_drawAxis() можно определить как функцию-член класса. example_drawAxis() для примера создаёт оси координат из примитивов-линий с помощью команд glBegin(GL_LINES) и glEnd(). Аргументом glBegin() является тип примитива, а в теле задаются вершины с помощью glVertex3f().
class Scene3D : public QGLWidget // новый класс Scene3D наследует встроенный класс QGLWidget { private: void example_drawAxis(); //... //... }; //... void Scene3D::example_drawAxis() { glLineWidth(3.0f); // устанавливаю ширину линии приближённо в пикселях // до вызова здесь команды ширина была равна 1 пикселю по умолчанию glColor4f(1.00f, 0.00f, 0.00f, 1.0f); // устанавливается цвет последующих примитивов // ось x красного цвета glBegin(GL_LINES); // построение линии glVertex3f( 1.0f, 0.0f, 0.0f); // первая точка glVertex3f(-1.0f, 0.0f, 0.0f); // вторая точка glEnd(); QColor halfGreen(0, 128, 0, 255); qglColor(halfGreen); glBegin(GL_LINES); // ось y зеленого цвета glVertex3f( 0.0f, 1.0f, 0.0f); glVertex3f( 0.0f, -1.0f, 0.0f); glColor4f(0.00f, 0.00f, 1.00f, 1.0f); // ось z синего цвета glVertex3f( 0.0f, 0.0f, 1.0f); glVertex3f( 0.0f, 0.0f, -1.0f); glEnd(); }
По умолчанию ось y будет направлена вверх в плоскости экрана, ось x вправо в плоскости экрана, а ось z будет перпендикулярна экрану и направлена из него на вас. Оси образуют правую тройку векторов, как и должно быть в стандартной математике. Центр системы координат будет расположен в центре экрана. Эта система координат показывает мировые координаты, которыми задаются трёхмерные объекты в программе.
Особенность библиотеки OpenGL в том, что она работает как машина с настройками (машина состояний, или конечный автомат). Если была установлена некоторая настройка, то она будет выполняться до тех пор, пока не будет изменена. Наглядный пример: команда glColor4f(), которая устанавливает цвет RGBA. Обратите внимание, что совместно использована функция qglColor(). То же самое относится и к командам glEnable() для включения и glDisable() для выключения настроек.
Представление примитивов с помощью команд glBegin() и glEnd() является очень наглядным и простым для понимания, тем не менее, недостаточно эффективным по быстродействию. Поэтому для повышения быстродействия в случаях статической геометрии их часто помещают в дисплейные списки (display lists, имеются также переводы: списки отображения, списки изображения, таблицы отображения). Ещё большей эффективности можно добиться, используя массивы вершин, массивы индексов вершин, массивы цветов вершин и т.д. Этот метод применим также в случае динамической геометрии. Пример использования массивов будет продемонстрирован в заключительном обобщающем листинге программы.
Наконец, рассмотрим слот updateGL(). Слоты являются как бы командами и используются Qt в механизме сигналов и слотов, но здесь этот механизм объясняться не будет. Слот updateGL() вызывает через glDraw() (функция класса QGLWidget, а не OpenGL) выполнение функции paintGL(), которая в свою очередь выполняет рисование сцены. Этот слот не нужно определять в своём классе, он просто берётся из QGLWidget. Пример:
void Scene3D::example_calculate() { // вычисления // преобразования // ... updateGL(); // вызывается функция paintGL() -> перерисование сцены -> обновление изображения }
[править] Рисуем икосаэдр
Мы рассмотрели основные функции Qt для работы с OpenGL и сопутствующие команды самого OpenGL. Но для минимального набора знаний нам также потребуется методы обработки нажатия клавиш и событий мыши, определенные в Qt. Всё это будет продемонстрировано в последнем листинге полной программы. А в качестве геометрической фигуры мы нарисуем икосаэдр, используя массив вершин. Программа компилировалась и тестировалась на Windows XP + Qt 4.3.0 и openSUSE 11.2 + Qt 4.5. Листинг состоит из трёх файлов: scene3D.h, scene3D.cpp и main.cpp.
scene3D.h
#ifndef SCENE3D_H #define SCENE3D_H #include <QGLWidget> // подключаем класс QGLWidget class Scene3D : public QGLWidget // класс Scene3D наследует встроенный класс QGLWidget { private: GLfloat xRot; // переменная хранит угол поворота вокруг оси X GLfloat yRot; // переменная хранит угол поворота вокруг оси Y GLfloat zRot; // переменная хранит угол поворота вокруг оси Z GLfloat zTra; // переменная хранит величину трансляции оси Z GLfloat nSca; // переменная отвечает за масштабирование обьекта QPoint ptrMousePosition; // переменная хранит координату указателя мыши в момент нажатия void scale_plus(); // приблизить сцену void scale_minus(); // удалиться от сцены void rotate_up(); // повернуть сцену вверх void rotate_down(); // повернуть сцену вниз void rotate_left(); // повернуть сцену влево void rotate_right(); // повернуть сцену вправо void translate_down(); // транслировать сцену вниз void translate_up(); // транслировать сцену вверх void defaultScene(); // наблюдение сцены по умолчанию void drawAxis(); // построить оси координат void getVertexArray(); // определить массив вершин void getColorArray(); // определить массив цветов вершин void getIndexArray(); // определить массив индексов вершин void drawFigure(); // построить фигуру protected: /*virtual*/ void initializeGL(); // метод для проведения инициализаций, связанных с OpenGL /*virtual*/ void resizeGL(int nWidth, int nHeight); // метод вызывается при изменении размеров окна виджета /*virtual*/ void paintGL(); // метод, чтобы заново перерисовать содержимое виджета /*virtual*/ void mousePressEvent(QMouseEvent* pe); // методы обработки события мыши при нажатии клавиши мыши /*virtual*/ void mouseMoveEvent(QMouseEvent* pe); // методы обработки события мыши при перемещении мыши /*virtual*/ void mouseReleaseEvent(QMouseEvent* pe); // методы обработки событий мыши при отжатии клавиши мыши /*virtual*/ void wheelEvent(QWheelEvent* pe); // метод обработки событий колесика мыши /*virtual*/ void keyPressEvent(QKeyEvent* pe); // методы обработки события при нажатии определенной клавиши public: Scene3D(QWidget* parent = 0); // конструктор класса }; #endif
Значение ptrMousePosition типа QPoint хранит координаты на экране: x и y. Они нам понадобятся для определения положения мыши. Класс QPoint находится в модуле QtCore, который здесь явно можно не подключать. В функциях getVertexArray(), getColorArray() и getIndexArray() инициализируются массив вершин VertexArray, массив цветов вершин ColorArray и массив индексов вершин IndexArray. Чуть позже мы поговорим о них подробнее. Функция drawFigure() строит икосаэдр, используя перечисленные массивы.
[править] Обработка пользовательского ввода
Функции обработки событий мыши mousePressEvent() и mouseMoveEvent() мы будем использовать для вращения сцены (на самом деле, вращения наблюдателя, но это эквивалентно). Первая функция вызывается нажатием клавиши мыши, вторая перемещением мыши. Нам нужно знать только, что вторая функция выполняется, когда клавиша мыши нажата — трекинг (отслеживание) выключен. Функцию wheelEvent() — обработка событий колёсика мыши — мы будем использовать для масштабирования сцены. Также приведена функция при отжатии клавиши мыши mouseReleaseEvent(), её удобно использовать, когда нужно определить, какое действие должно выполниться мышью: например, изменение наблюдения сцены или какое-то изменение на сцене. Функция keyPressEvent() вызывается при нажатии клавиши. Все перечисленные функции принадлежат классу QWidget и переопределяются (динамический полиморфизм).
scene3D.cpp
#include <QtGui> // подключаем модуль QtGui //#include <QtCore> // подключаем модуль QtCore //#include <QtOpenGL> // подключаем модуль QtOpenGL #include <math.h> // подключаем математическую библиотеку #include "scene3D.h" // подключаем заголовочный файл scene3D.h const static float pi=3.141593, k=pi/180; // глобальная переменная GLfloat VertexArray[12][3]; // декларируем массив вершин GLfloat ColorArray[12][3]; // декларируем массив цветов вершин GLubyte IndexArray[20][3]; // декларируем массив индексов вершин Scene3D::Scene3D(QWidget* parent/*= 0*/) : QGLWidget(parent) // конструктор класса Scene3D { // setFormat(QGLFormat(QGL::DepthBuffer)); // использовать буфер глубины // установлено по умолчанию в контексте // начальные значения xRot=-90; yRot=0; zRot=0; zTra=0; nSca=1; // передает дальше указатель на объект pwgt } /*virtual*/ void Scene3D::initializeGL() // инициализация { qglClearColor(Qt::white); // цвет для очистки буфера изображения - здесь просто фон окна glEnable(GL_DEPTH_TEST); // устанавливает режим проверки глубины пикселей glShadeModel(GL_FLAT); // отключает режим сглаживания цветов glEnable(GL_CULL_FACE); // устанавливается режим, когда строятся только внешние поверхности getVertexArray(); // определить массив вершин getColorArray(); // определить массив цветов вершин getIndexArray(); // определить массив индексов вершин glEnableClientState(GL_VERTEX_ARRAY); // активизация массива вершин glEnableClientState(GL_COLOR_ARRAY); // активизация массива цветов вершин } /*virtual*/void Scene3D::resizeGL(int nWidth, int nHeight) // окно виджета { glMatrixMode(GL_PROJECTION); // устанавливает текущей проекционную матрицу glLoadIdentity(); // присваивает проекционной матрице единичную матрицу GLfloat ratio=(GLfloat)nHeight/(GLfloat)nWidth; // отношение высоты окна виджета к его ширине // мировое окно if (nWidth>=nHeight) glOrtho(-1.0/ratio, 1.0/ratio, -1.0, 1.0, -10.0, 1.0); // параметры видимости ортогональной проекции else glOrtho(-1.0, 1.0, -1.0*ratio, 1.0*ratio, -10.0, 1.0); // параметры видимости ортогональной проекции // плоскости отсечения (левая, правая, верхняя, нижняя, передняя, задняя) // glFrustum(-1.0, 1.0, -1.0, 1.0, 1.0, 10.0); // параметры видимости перспективной проекции // плоскости отсечения (левая, правая, верхняя, нижняя, ближняя, дальняя) // поле просмотра glViewport(0, 0, (GLint)nWidth, (GLint)nHeight); } /*virtual*/ void Scene3D::paintGL() // рисование { // glClear(GL_COLOR_BUFFER_BIT); // окно виджета очищается текущим цветом очистки glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // очистка буфера изображения и глубины glMatrixMode(GL_MODELVIEW); // устанавливает положение и ориентацию матрице моделирования glLoadIdentity(); // загружает единичную матрицу моделирования // последовательные преобразования glScalef(nSca, nSca, nSca); // масштабирование glTranslatef(0.0f, zTra, 0.0f); // трансляция 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 drawAxis(); // рисование осей координат drawFigure(); // нарисовать фигуру } /*virtual*/void Scene3D::mousePressEvent(QMouseEvent* pe) // нажатие клавиши мыши { // при нажатии пользователем кнопки мыши переменной ptrMousePosition будет // присвоена координата указателя мыши ptrMousePosition = pe->pos(); // ptrMousePosition = (*pe).pos(); // можно и так написать } /*virtual*/void Scene3D::mouseReleaseEvent(QMouseEvent* pe) // отжатие клавиши мыши { // некоторые функции, которые должны выполняться при отжатии клавиши мыши } /*virtual*/void Scene3D::mouseMoveEvent(QMouseEvent* pe) // изменение положения стрелки мыши { xRot += 180/nSca*(GLfloat)(pe->y()-ptrMousePosition.y())/height(); // вычисление углов поворота zRot += 180/nSca*(GLfloat)(pe->x()-ptrMousePosition.x())/width(); ptrMousePosition = pe->pos(); updateGL(); // обновление изображения } /*virtual*/void Scene3D::wheelEvent(QWheelEvent* pe) // вращение колёсика мыши { if ((pe->delta())>0) scale_plus(); else if ((pe->delta())<0) scale_minus(); updateGL(); // обновление изображения } /*virtual*/void Scene3D::keyPressEvent(QKeyEvent* pe) // нажатие определенной клавиши { switch (pe->key()) { case Qt::Key_Plus: scale_plus(); // приблизить сцену break; case Qt::Key_Equal: scale_plus(); // приблизить сцену break; case Qt::Key_Minus: scale_minus(); // удалиться от сцены break; case Qt::Key_Up: rotate_up(); // повернуть сцену вверх break; case Qt::Key_Down: rotate_down(); // повернуть сцену вниз break; case Qt::Key_Left: rotate_left(); // повернуть сцену влево break; case Qt::Key_Right: rotate_right(); // повернуть сцену вправо break; case Qt::Key_Z: translate_down(); // транслировать сцену вниз break; case Qt::Key_X: translate_up(); // транслировать сцену вверх break; case Qt::Key_Space: // клавиша пробела defaultScene(); // возвращение значений по умолчанию break; case Qt::Key_Escape: // клавиша "эскейп" this->close(); // завершает приложение break; } updateGL(); // обновление изображения } void Scene3D::scale_plus() // приблизить сцену { nSca = nSca*1.1; } void Scene3D::scale_minus() // удалиться от сцены { nSca = nSca/1.1; } void Scene3D::rotate_up() // повернуть сцену вверх { xRot += 1.0; } void Scene3D::rotate_down() // повернуть сцену вниз { xRot -= 1.0; } void Scene3D::rotate_left() // повернуть сцену влево { zRot += 1.0; } void Scene3D::rotate_right() // повернуть сцену вправо { zRot -= 1.0; } void Scene3D::translate_down() // транслировать сцену вниз { zTra -= 0.05; } void Scene3D::translate_up() // транслировать сцену вверх { zTra += 0.05; } void Scene3D::defaultScene() // наблюдение сцены по умолчанию { xRot=-90; yRot=0; zRot=0; zTra=0; nSca=1; } void Scene3D::drawAxis() // построить оси координат { glLineWidth(3.0f); // устанавливаю ширину линии приближённо в пикселях // до вызова команды ширина равна 1 пикселю по умолчанию glColor4f(1.00f, 0.00f, 0.00f, 1.0f); // устанавливается цвет последующих примитивов // ось x красного цвета glBegin(GL_LINES); // построение линии glVertex3f( 1.0f, 0.0f, 0.0f); // первая точка glVertex3f(-1.0f, 0.0f, 0.0f); // вторая точка glEnd(); QColor halfGreen(0, 128, 0, 255); qglColor(halfGreen); glBegin(GL_LINES); // ось y зеленого цвета glVertex3f( 0.0f, 1.0f, 0.0f); glVertex3f( 0.0f, -1.0f, 0.0f); glColor4f(0.00f, 0.00f, 1.00f, 1.0f); // ось z синего цвета glVertex3f( 0.0f, 0.0f, 1.0f); glVertex3f( 0.0f, 0.0f, -1.0f); glEnd(); } void Scene3D::getVertexArray() // определить массив вершин { GLfloat R=0.75; // радиус сферы // начальные значения для икосаэдра GLfloat a=4*R/sqrt(10+2*sqrt(5)); // сторона икосаэдра GLfloat alpha=acos((1-a*a/2/R/R)); // первый угол поворота по тэта // cos(alpha)=(1-a*a/2/R/R) // вычисляем точки икосаэдра //0 точка VertexArray[0][0]=0; // x VertexArray[0][1]=0; // y VertexArray[0][2]=R; // z //1 точка VertexArray[1][0]=R*sin(alpha)*sin(0); VertexArray[1][1]=R*sin(alpha)*cos(0); VertexArray[1][2]=R*cos(alpha); //2 точка VertexArray[2][0]=R*sin(alpha)*sin(72*k); VertexArray[2][1]=R*sin(alpha)*cos(72*k); VertexArray[2][2]=R*cos(alpha); //3 точка VertexArray[3][0]=R*sin(alpha)*sin(2*72*k); VertexArray[3][1]=R*sin(alpha)*cos(2*72*k); VertexArray[3][2]=R*cos(alpha); //4 точка VertexArray[4][0]=R*sin(alpha)*sin(3*72*k); VertexArray[4][1]=R*sin(alpha)*cos(3*72*k); VertexArray[4][2]=R*cos(alpha); //5 точка VertexArray[5][0]=R*sin(alpha)*sin(4*72*k); VertexArray[5][1]=R*sin(alpha)*cos(4*72*k); VertexArray[5][2]=R*cos(alpha); //6 точка VertexArray[6][0]=R*sin(pi-alpha)*sin(-36*k); VertexArray[6][1]=R*sin(pi-alpha)*cos(-36*k); VertexArray[6][2]=R*cos(pi-alpha); //7 точка VertexArray[7][0]=R*sin(pi-alpha)*sin(36*k); VertexArray[7][1]=R*sin(pi-alpha)*cos(36*k); VertexArray[7][2]=R*cos(pi-alpha); //8 точка VertexArray[8][0]=R*sin(pi-alpha)*sin((36+72)*k); VertexArray[8][1]=R*sin(pi-alpha)*cos((36+72)*k); VertexArray[8][2]=R*cos(pi-alpha); //9 точка VertexArray[9][0]=R*sin(pi-alpha)*sin((36+2*72)*k); VertexArray[9][1]=R*sin(pi-alpha)*cos((36+2*72)*k); VertexArray[9][2]=R*cos(pi-alpha); //10 точка VertexArray[10][0]=R*sin(pi-alpha)*sin((36+3*72)*k); VertexArray[10][1]=R*sin(pi-alpha)*cos((36+3*72)*k); VertexArray[10][2]=R*cos(pi-alpha); //11 точка VertexArray[11][0]=0; VertexArray[11][1]=0; VertexArray[11][2]=-R; } void Scene3D::getColorArray() // определить массив цветов вершин { for (int i=0; i<12; i++) { ColorArray[i][0]=0.1f*(qrand()%11); // R - красная составляющая ColorArray[i][1]=0.1f*(qrand()%11); // G - зелёная составляющая ColorArray[i][2]=0.1f*(qrand()%11); // B - синяя составляющая // qrand()%11 - псевдослучайное число от 0 до 10 } } 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]=5; IndexArray[3][2]=4; // 4 треугольник IndexArray[4][0]=0; IndexArray[4][1]=1; IndexArray[4][2]=5; // 5 треугольник IndexArray[5][0]=6; IndexArray[5][1]=1; IndexArray[5][2]=7; // 6 треугольник IndexArray[6][0]=7; IndexArray[6][1]=1; IndexArray[6][2]=2; // 7 треугольник IndexArray[7][0]=7; IndexArray[7][1]=2; IndexArray[7][2]=8; // 8 треугольник IndexArray[8][0]=8; IndexArray[8][1]=2; IndexArray[8][2]=3; // 9 треугольник IndexArray[9][0]=8; IndexArray[9][1]=3; IndexArray[9][2]=9; // 10 треугольник IndexArray[10][0]=9; IndexArray[10][1]=3; IndexArray[10][2]=4; // 11 треугольник IndexArray[11][0]=9; IndexArray[11][1]=4; IndexArray[11][2]=10; // 12 треугольник IndexArray[12][0]=10; IndexArray[12][1]=4; IndexArray[12][2]=5; // 13 треугольник IndexArray[13][0]=10; IndexArray[13][1]=5; IndexArray[13][2]=6; // 14 треугольник IndexArray[14][0]=6; IndexArray[14][1]=5; IndexArray[14][2]=1; // 15 треугольник IndexArray[15][0]=7; IndexArray[15][1]=11; IndexArray[15][2]=6; // 16 треугольник IndexArray[16][0]=8; IndexArray[16][1]=11; IndexArray[16][2]=7; // 17 треугольник IndexArray[17][0]=9; IndexArray[17][1]=11; IndexArray[17][2]=8; // 18 треугольник IndexArray[18][0]=10; IndexArray[18][1]=11; IndexArray[18][2]=9; // 19 треугольник IndexArray[19][0]=6; IndexArray[19][1]=11; IndexArray[19][2]=10; } void Scene3D::drawFigure() // построить фигуру { glVertexPointer(3, GL_FLOAT, 0, VertexArray); // указываем, откуда нужно извлечь данные о массиве вершин glColorPointer(3, GL_FLOAT, 0, ColorArray); // указываем, откуда нужно извлечь данные о массиве цветов вершин // используя массивы вершин и индексов, glDrawElements(GL_TRIANGLES, 60, GL_UNSIGNED_BYTE, IndexArray); // строим поверхности }
[править] Используя массивы вершин
В начале программы мы декларируем массивы вершин, цветов и индексов. Чтобы иметь представление, скажу, что икосаэдр имеет 12 вершин и 20 (равносторонних) треугольников. В массив вершин мы запишем декартовы координаты вершин: x, y, z. В массив цветов запишем цвета вершин с тремя составляющими RGB, сгенерировав их псевдослучайно. В массив индексов запишем индексы вершин — три номера (индекса) вершин. Для чего это всё нужно? Чтобы построить какую-нибудь фигуру, мы должны как минимум знать вершины фигуры и знать каким способом из них построить примитивы, например, треугольники. Поэтому в двумерный массив вершин мы заносим координаты вершин, а в массив индексов (фактически массив треугольников) записываем последовательные номера вершин (фактически три последовательные вершины треугольника). Каждой вершине также можно дать цвет — для этого и служит массив цветов. Координаты вершин мы рассчитываем сами, индексы примитива также задаём самостоятельно. Но как вы понимаете, их можно загрузить из файла. А перед этим создать модель в визуальном 3D-графическом редакторе, например в Blender, сохранить модель в файл и, зная структуру файла, сделать загрузчик моделей.
[править] Настройки
В конструкторе класса Scene3D мы задаём начальные значения. Команда setFormat(QGLFormat(QGL::DepthBuffer)) указывает, что нужно использовать буфер глубины, о котором говорилось раньше. Эта команда эквивалентна glutInitDisplayMode(GLUT_DEPHT) библиотеки GLUT. Соответственно, команда setFormat() класса QGLContext устанавливает контекст (режим работы) OpenGL. Вообще говоря, буфер глубины можно явно и не устанавливать, т.к. он по умолчанию автоматически устанавливается в контекст. Так же как и состояние RGBA (RGBA mode). Обратите внимание, что в конструкторе мы поворачиваем сцену вокруг оси x на -90 градусов.
При инициализации initializeGL() мы отключаем режим сглаживания цветов командой glShadeModel(GL_FLAT). В этом случае примитив закрашивается цветом последней вершины. Команда glShadeModel(GL_SMOOTH) установит обратно режим сглаживания цветов, если это будет нужно.
Чтобы активировать массивы вершин и цветов, используются соответственно команды glEnableClientState(GL_VERTEX_ARRAY) и glEnableClientState(GL_COLOR_ARRAY).
В функции resizeGL() мы изменяем мировое окно и подстраиваем его под размеры окна виджета. Теперь сцена не будет растягиваться по ширине при развертывании окна на весь экран. Мы могли бы и не изменять мировое окно, а преобразовать поле просмотра.
Попробуйте закомментировать glOrtho() и раскомментировать glFrustum(). В результате вы увидите пустое окно. Подумайте почему)
[править] Ещё раз про обработку сигналов
Как уже говорилось, функция mousePressEvent() вызывается автоматически при нажатии клавиши мыши. В теле этой функции мы запоминаем координату мыши в момент нажатия с помощью метода pos() класса QPoint. Координата мыши понадобится нам при вычислении углов поворота в теле mouseMoveEvent(), используя методы x(), y() класса QPoint и методы height() и width() класса QWidget. Обратите внимание, например, на такую запись Qt::Key_Escape в функции keyPressEvent(). В данном случае Qt есть пространство имен (namespace), которому принадлежит константа Key_Escape. Эта константа имеет значение 0x01000000 (шестнадцатиричная система). Очевидно, что при нажатии клавиши должно производиться сравнение с указанными константными значениями клавиш. Вам не составит труда самостоятельно разобраться, что происходит в функциях wheelEvent(), keyPressEvent(), а так же в scale_plus(), scale_minus(), rotate_up(), rotate_down(), rotate_left(), rotate_right(), translate_down(), translate_up() и defaultScene().
[править] Собираем всё вместе и не только!
Наконец, в функции drawFigure() мы строим фигуру с помощью массивов. Сначала указываем, откуда нужно извлечь данные о массивах, т.е. какие массивы мы будем использовать. Это осуществляют команды glVertexPointer() и glColorPointer(). Потом указываем, что и как нужно построить с помощью glDrawElements(). Недостатком использования этих методов является то, что мы не сможем построить примитивы своими собственными цветами, как если бы мы рисовали через glBegin() и glEnd() с предварительной командой glColor4f(). Поэтому мы пошли на хитрость, а какую именно, подумайте сами.
main.cpp
#include <QApplication> #include "scene3D.h" int main(int argc, char** argv) { QApplication app(argc, argv); // создаём приложение, инициализация оконной системы Scene3D scene1; // создаём виджет класса Scene3D scene1.setWindowTitle("lecture1"); // название окна scene1.resize(500, 500); // размеры (nWidth, nHeight) окна scene1.show(); // изобразить виджет // scene1.showFullScreen(); // scene1.showMaximized(); return app.exec(); }
В главной функции main() мы создаём объект графического интерфейса класса QApplication, который осуществляет контроль и управление приложением. Потом создаём виджет (объект верхнего уровня, т.к. не заданы никакие аргументы и берётся значение по умолчанию) нашего класса Scene3D, задаём его отображаемое название и размеры (nWidth, nHeight) и указываем, во-первых, что его нужно изобразить и, во-вторых, при необходимости как его изобразить. Вызов функции exec(), принадлежащей классу QApplication, производит запуск приложения и сопутствующий контроль.
В файле-проекте .pro необходимо добавить следующую строку:
QT += opengl
для сборки приложения совместно с модулем QtOpenGL и системной библиотекой OpenGL.
Замечу, что если бы вы использовали API Windows для создания GUI, то код получился бы на порядки больше и сложнее для понимания. Вы бы потратили колоссальное количество времени, чтобы его освоить и в нём разобраться, при этом ваш код оказался бы полностью бесполезным в Linux и MacOS. Чего нельзя сказать об использовании кроссплатформенной библиотеки Qt. Например, вот как будет выглядеть приложение, откомпилированное на openSUSE 11.2 (Linux), без изменения кода!
Надеюсь, из этой статьи вы открыли что-то новое и интересное для себя. Я дал вам лишь небольшой начальный импульс, насколько сильно вы его приумножите, зависит от вас. Спасибо за внимание.
[править] Литература
(в порядке ранжирования)
Для тех, кто незнаком с C++, рекомендую вам следующие книги:
- Аверкин В.П., Бобровский А.И., Веснич В.В., Радушинский В.Ф., Хомоненко А.Д. «Программирование на C++» (очень хорошая книга для начинающих)
- Дейтел Х., Дейтел П. «Как программировать на C++» (ОЧЕНЬ БОЛЬШАЯ книга, в ней много всего полезного)
Литература по OpenGL:
- Райт Р.С.-мл., Липчак Б. «OpenGL. Суперкнига» (отличная книга по OpenGL)
Литература по Qt 4:
- Qt Assistant и Qt Examples and Demos (помощь и много нужных примеров, поставляются вместе с Qt)
- Бланшет Ж., Саммерфилд М. «QT 4: программирование GUI на C++»
- Шлее М. «Qt4. Профессиональное программирование на C++»
Дублирование авторской статьи: http://www.gamedev.ru/code/articles/OpenGL_Qt4
Список частей:
- Работа с OpenGL на Qt 4 (часть 1)
- Работа с OpenGL на Qt 4 (часть 2)
(c) registr
Если вы обнаружили ошибки и неточности, то сообщите, пожалуйста, о них в комментариях, либо сообщите по эл. почте: registrcontact@mail.ru.