Редактирование: Работа с OpenGL на Qt 4 (часть 1)
Материал из Wiki.crossplatform.ru
Внимание: Вы не представились системе. Ваш IP-адрес будет записан в историю изменений этой страницы.
ПРЕДУПРЕЖДЕНИЕ: Длина этой страницы составляет 72 килобайт. Страницы, размер которых приближается к 32 КБ или превышает это значение, могут неверно отображаться в некоторых браузерах. Пожалуйста, рассмотрите вариант разбиения страницы на меньшие части.
Правка может быть отменена. Пожалуйста, просмотрите сравнение версий, чтобы убедиться, что это именно те изменения, которые вас интересуют, и нажмите «Записать страницу», чтобы изменения вступили в силу.
Текущая версия | Ваш текст | ||
Строка 12: | Строка 12: | ||
==Модуль QtOpenGL== | ==Модуль 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. | + | Библиотека 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. |
Классы подключаются стандартным образом: | Классы подключаются стандартным образом: | ||
Строка 76: | Строка 76: | ||
public: | public: | ||
- | Scene3D(QWidget* parent = 0); // конструктор класса Scene3D | + | Scene3D(QWidget* parent = 0) ; // конструктор класса Scene3D |
}; | }; | ||
Строка 89: | Строка 89: | ||
Класс виджета, который будет работать с OpenGL, должен наследоваться от класса QGLWidget. Мы назвали этот класс Scene3D. Объектом класса Scene3D будет виджет, в котором будет рисоваться трёхмерная сцена. Конструктор класса Scene3D сначала вызовит конструктор класса QGLWidget, который в свою очередь вызовит конструктор класса QWidget. | Класс виджета, который будет работать с OpenGL, должен наследоваться от класса QGLWidget. Мы назвали этот класс Scene3D. Объектом класса Scene3D будет виджет, в котором будет рисоваться трёхмерная сцена. Конструктор класса Scene3D сначала вызовит конструктор класса QGLWidget, который в свою очередь вызовит конструктор класса QWidget. | ||
- | В Qt имеет место так называемая иерархия объектов (класса QObject или его классов-наследников), когда связь между объектами образует древовидную структуру | + | В Qt имеет место так называемая иерархия объектов (класса QObject или его классов-наследников), когда связь между объектами образует древовидную структуру сверху вниз. Эту структуру называют деревом объектов. На вершине иерархии находится объект, от которого идёт вниз связь с другими объектами. Объект на вершине иерархии объектов называется объектом верхнего уровня. Он, являясь (как бы) объектом-родителем, связан с нижележащими объектами, которые являются для него (как бы) объектами-потомками. Эти объекты-потомки могут быть объектами-родителями для следующих нижележащих объектов и т.д. В Qt при уничтожении объекта-родителя все его объекты-потомки уничтожаются автоматически. Такая связь эффективна для управления памятью, когда достаточно удалить только верхний в иерархии объект и все связанные с ним нижележащие объекты удалятся автоматически. Здесь важно заметить, что объекты-потомки должны создаваться динамически (через new). В обобщающей программе, которая будет приведена во второй половине статьи, наш объект класса Scene3D будет объектом верхнего уровня. В конечном итоге в конструктор класса QWidget передастся значение 0, что и будет означать, что наш объект класса Scene3D является объектом верхнего уровня в иерархии объектов. |
В конструкторе Scene3D() нужно задать начальные значения, не являющиеся как таковыми командами OpenGL, например, углы поворотов при наблюдении сцены. Определять деструктор в данном случае необязательно; деструктор по умолчанию сам создастся компилятором. Функции initializeGL(), resizeGL(), paintGL() являются виртуальными функциями (подробнее о динамическом полиморфизме можно прочитать, например, в книге: Аверкин В.П., Бобровский А.И., Веснич В.В., Радушинский В.Ф., Хомоненко А.Д. «Программирование на C++»). Следовательно, в классе-наследнике Scene3D обозначать функции как virtual не нужно. Эти три функции изначально имеют полностью пустое тело, которое нужно заполнить командами OpenGL. Здесь нужно отметить, что функции glDraw() и glInit() принадлежат классу QGLWidget, а не к OpenGL (как можно было бы подумать из префикса gl). Функции glDraw() и glInit() не нужны для пользовательской работы; они просто вызывают initializeGL(), resizeGL(), paintGL(), о которых и пойдёт речь далее. Как раз только эти три функци и используются для пользовательской работы с OpenGL: при инициализации настроек рендеринга, при изменении размера окна виджета и при рисовании. | В конструкторе Scene3D() нужно задать начальные значения, не являющиеся как таковыми командами OpenGL, например, углы поворотов при наблюдении сцены. Определять деструктор в данном случае необязательно; деструктор по умолчанию сам создастся компилятором. Функции initializeGL(), resizeGL(), paintGL() являются виртуальными функциями (подробнее о динамическом полиморфизме можно прочитать, например, в книге: Аверкин В.П., Бобровский А.И., Веснич В.В., Радушинский В.Ф., Хомоненко А.Д. «Программирование на C++»). Следовательно, в классе-наследнике Scene3D обозначать функции как virtual не нужно. Эти три функции изначально имеют полностью пустое тело, которое нужно заполнить командами OpenGL. Здесь нужно отметить, что функции glDraw() и glInit() принадлежат классу QGLWidget, а не к OpenGL (как можно было бы подумать из префикса gl). Функции glDraw() и glInit() не нужны для пользовательской работы; они просто вызывают initializeGL(), resizeGL(), paintGL(), о которых и пойдёт речь далее. Как раз только эти три функци и используются для пользовательской работы с OpenGL: при инициализации настроек рендеринга, при изменении размера окна виджета и при рисовании. | ||
Строка 119: | Строка 119: | ||
В соответствии с правилами обозначения в OpenGL значение 1.0f обрабатывается как тип GLfloat (собственный тип OpenGL). Зачем нужны собственные типы? Дело в том, что различные компиляторы и платформы по-разному распределяют память под стандартные типы и программист должен всегда это держать в уме. Собственные типы OpenGL обрабатываются везде одинаково и освобождают нас от этого обременительного занятия. | В соответствии с правилами обозначения в OpenGL значение 1.0f обрабатывается как тип GLfloat (собственный тип OpenGL). Зачем нужны собственные типы? Дело в том, что различные компиляторы и платформы по-разному распределяют память под стандартные типы и программист должен всегда это держать в уме. Собственные типы OpenGL обрабатываются везде одинаково и освобождают нас от этого обременительного занятия. | ||
- | Следующая функция glEnable(GL_DEPTH_TEST) является уже функцией OpenGL, что можно понять из приставки gl в отличие от qgl для первой функции. | + | Следующая функция glEnable(GL_DEPTH_TEST) является уже функцией OpenGL, что можно понять из приставки gl в отличие от qgl для первой функции. glEnable(GL_DEPTH_TEST) устанавливает режим проверки глубины объектов, известных также как z-буфер (z-buffer) или буфер глубины. Суть его заключается в том, что каждому пикселю объекта на экране с координатами x и y даётся еще координата z, характеризующая расстояние до наблюдателя. Как раз значения глубины для каждого пикселя объекта хранятся в буфере глубины. Таким образом, одни объекты могут быть ближе к нам, а другие дальше, а значит, ближние объекты могут закрыть собой дальние. Зачем рисовать дальние объекты, если они невидны? Правильно, незачем! Поэтому нужно иметь метод, чтобы не рисовать скрытые (невидимые) поверхности («закрытые» пиксели). Если при рисовании сравнивать глубину для каждого пикселя всех объектов, то можно определить виден ли конкретный пиксель объекта на экране или скрыт. Как раз это и выполняет glEnable(GL_DEPTH_TEST), т.е. производит сравнение. Также нужно будет сообщить системе об использовании буфера глубины. Qt автоматически в конструкторе класса Scene3D по умолчанию задаст такой формат в контекст, т.е. использовать буфер глубины. Поэтому буфер глубины можно явно и не подключать, он подключится сам. Использование буфера глубины и сравнение глубины повышают скорость вывода изображения за счёт невидимых поверхностей. |
Закомментированная функция glShadeModel(GL_FLAT) отключает режим сглаживания цветов, который всегда установлен по умолчанию. Если вершины имеют разный цвет, то цвет между ними будет плавно переходить из одного в другой. Отключать режим сглаживания нет необходимости, поэтому функция закомментирована и не используется. | Закомментированная функция glShadeModel(GL_FLAT) отключает режим сглаживания цветов, который всегда установлен по умолчанию. Если вершины имеют разный цвет, то цвет между ними будет плавно переходить из одного в другой. Отключать режим сглаживания нет необходимости, поэтому функция закомментирована и не используется. | ||
Строка 152: | Строка 152: | ||
Функция glViewport(0, 0, (GLint)nWidth, (GLint)nHeight) определяет поле просмотра (порт просмотра) внутри окна в пикселях экрана и образует прямоугольник с левой нижней точкой (0, 0) и правой верхней точкой (nWidth, nHeight). В этом поле и будет всё рисоваться. Для нас важно, что прямоугольник поля просмотра совпадает с прямоугольником виджета окна. Так как nWidth и nHeight типа int, стоит произвести преобразования к типам GLint. | Функция glViewport(0, 0, (GLint)nWidth, (GLint)nHeight) определяет поле просмотра (порт просмотра) внутри окна в пикселях экрана и образует прямоугольник с левой нижней точкой (0, 0) и правой верхней точкой (nWidth, nHeight). В этом поле и будет всё рисоваться. Для нас важно, что прямоугольник поля просмотра совпадает с прямоугольником виджета окна. Так как nWidth и nHeight типа int, стоит произвести преобразования к типам GLint. | ||
- | Часто новички, задавая одинаковые первоначальные значения ширины и высоты, недоумевают почему их изображение растягивается по ширине при развертывании окна на весь экран. Ничего удивительного в этом нет — ширина становится больше по значению, чем высота. И OpenGL автоматически масштабирует всю проекцию сцены так, что она становится вытянутой по координате x экрана. Чуть позже в заключительном листинге обобщающей программы будет показано, как этого избежать и «сохранить квадрат квадратным». Также необходимо помнить, что отсчёт координаты y на экране производиться по-разному: из самой нижней точки вверх в OpenGL и из самой верхней точки вниз в экранных координатах в | + | Часто новички, задавая одинаковые первоначальные значения ширины и высоты, недоумевают почему их изображение растягивается по ширине при развертывании окна на весь экран. Ничего удивительного в этом нет — ширина становится больше по значению, чем высота. И OpenGL автоматически масштабирует всю проекцию сцены так, что она становится вытянутой по координате x экрана. Чуть позже в заключительном листинге обобщающей программы будет показано, как этого избежать и «сохранить квадрат квадратным». Также необходимо помнить, что отсчёт координаты y на экране производиться по-разному: из самой нижней точки вверх в OpenGL и из самой верхней точки вниз в экранных координатах в OS Windows. |
Построение изображения в OpenGL происходит по следующему принципу: (мировые координаты)-->(мировое окно)-->(поле просмотра). | Построение изображения в OpenGL происходит по следующему принципу: (мировые координаты)-->(мировое окно)-->(поле просмотра). | ||
Строка 186: | Строка 186: | ||
Далее приведены последовательные преобразования: поворот, трансляция, масштабирование. Значения (GLfloat) xRot, yRot, zRot, xTra, yTra, zTra, xSca, ySca, zSca удобно определить как private данные-члены класса и задать им начальные значения в теле конструктора класса. Как вы догадались, это углы поворота вокруг трёх осей, величины трансляции и масштабирования. Особенность углов поворота в том, что они задаются в градусах, а не в радианах. Функция glRotatef() осуществляет поворот наблюдателя на заданный угол вокруг заданной оси. Соответственно, функции glTranslatef() и glScalef() производят трансляцию сцены и масштабирование. Важно напомнить ещё раз, что любое преобразование изменяет текущие матрицы. И следующее преобразование осуществляется относительно предыдущего. Поэтому операции (поворот->трансляция) и (трансляция->поворот) приводят к разным результатам; операции (поворот1->поворот2) и (поворот2->поворот1) также могут привести к разным конечным преобразованиям. Все эти преобразования в итоге изменяют матрицу моделирования от единичной. Именно поэтому необходимо при перерисовании сцены загружать единичную матрицу, после чего будут осуществляться указанные преобразования над ней. К тому же вы можете загружать матрицы, сделанные «своими руками». | Далее приведены последовательные преобразования: поворот, трансляция, масштабирование. Значения (GLfloat) xRot, yRot, zRot, xTra, yTra, zTra, xSca, ySca, zSca удобно определить как private данные-члены класса и задать им начальные значения в теле конструктора класса. Как вы догадались, это углы поворота вокруг трёх осей, величины трансляции и масштабирования. Особенность углов поворота в том, что они задаются в градусах, а не в радианах. Функция glRotatef() осуществляет поворот наблюдателя на заданный угол вокруг заданной оси. Соответственно, функции glTranslatef() и glScalef() производят трансляцию сцены и масштабирование. Важно напомнить ещё раз, что любое преобразование изменяет текущие матрицы. И следующее преобразование осуществляется относительно предыдущего. Поэтому операции (поворот->трансляция) и (трансляция->поворот) приводят к разным результатам; операции (поворот1->поворот2) и (поворот2->поворот1) также могут привести к разным конечным преобразованиям. Все эти преобразования в итоге изменяют матрицу моделирования от единичной. Именно поэтому необходимо при перерисовании сцены загружать единичную матрицу, после чего будут осуществляться указанные преобразования над ней. К тому же вы можете загружать матрицы, сделанные «своими руками». | ||
- | Теперь можно полностью представить, как происходит долгий путь построения изображения в OpenGL: (мировые координаты)--> ... -->(координаты в окне). Конвейер преобразований: (мировые координаты)-->[матрица моделирования]-->(преобразованные координаты)-->[матрица проекции]-->(мировое окно - координаты с отсечением)-->[перспективное преобразование]-->(преобразованные координаты)-->[преобразование поля просмотра]-->(координаты в окне) | + | Теперь можно полностью представить, как происходит долгий путь построения изображения в OpenGL: (мировые координаты)--> ... -->(координаты в окне). Конвейер преобразований: (мировые координаты)-->[матрица моделирования]-->(преобразованные координаты)-->[матрица проекции]-->(мировое окно - координаты с отсечением)-->[перспективное преобразование]-->(преобразованные координаты)-->[преобразование поля просмотра]-->(координаты в окне). |
Нашу последнюю функцию example_drawAxis() можно определить как функцию-член класса. example_drawAxis() для примера создаёт оси координат из примитивов-линий с помощью команд glBegin(GL_LINES) и glEnd(). Аргументом glBegin() является тип примитива, а в теле задаются вершины с помощью glVertex3f(). | Нашу последнюю функцию example_drawAxis() можно определить как функцию-член класса. example_drawAxis() для примера создаёт оси координат из примитивов-линий с помощью команд glBegin(GL_LINES) и glEnd(). Аргументом glBegin() является тип примитива, а в теле задаются вершины с помощью glVertex3f(). | ||
Строка 303: | Строка 303: | ||
public: | public: | ||
- | Scene3D(QWidget* parent = 0); // конструктор класса | + | Scene3D(QWidget* parent = 0) ; // конструктор класса (= 0 -- главное окно) |
}; | }; | ||
#endif | #endif | ||
Строка 777: | Строка 777: | ||
QApplication app(argc, argv); // создаём приложение, инициализация оконной системы | QApplication app(argc, argv); // создаём приложение, инициализация оконной системы | ||
- | + | scene1 = new Scene3D; // создаём виджет класса Scene3D | |
- | scene1 | + | scene1->setWindowTitle("lecture1"); // название окна |
- | scene1 | + | scene1->resize(500, 500); // размеры (nWidth, nHeight) окна |
- | scene1 | + | scene1->show(); // изобразить виджет |
- | // scene1 | + | // scene1->showFullScreen(); |
- | // scene1 | + | // scene1->showMaximized(); |
return app.exec(); | return app.exec(); | ||
Строка 789: | Строка 789: | ||
---- | ---- | ||
В главной функции main() мы создаём объект графического интерфейса класса QApplication, который осуществляет контроль и управление приложением. Потом создаём виджет (объект верхнего уровня, т.к. не заданы никакие аргументы и берётся значение по умолчанию) нашего класса Scene3D, задаём его отображаемое название и размеры (nWidth, nHeight) и указываем, во-первых, что его нужно изобразить и, во-вторых, при необходимости как его изобразить. Вызов функции exec(), принадлежащей классу QApplication, производит запуск приложения и сопутствующий контроль. | В главной функции main() мы создаём объект графического интерфейса класса QApplication, который осуществляет контроль и управление приложением. Потом создаём виджет (объект верхнего уровня, т.к. не заданы никакие аргументы и берётся значение по умолчанию) нашего класса Scene3D, задаём его отображаемое название и размеры (nWidth, nHeight) и указываем, во-первых, что его нужно изобразить и, во-вторых, при необходимости как его изобразить. Вызов функции exec(), принадлежащей классу QApplication, производит запуск приложения и сопутствующий контроль. | ||
+ | |||
+ | Очень важно отметить, что Qt самостоятельно отслеживает освобождение памяти от динамических объектов. Объекты лучше создавать динамически, а программисту в этом случае не нужно следить за освобождением памяти от динамических объектов и не нужно удалять их через delete в деструкторах. | ||
В файле-проекте .pro необходимо добавить следующую строку: | В файле-проекте .pro необходимо добавить следующую строку: |