OpenGL на Qt4. Это просто! (статья), ознакомительная статья |
Здравствуйте, гость ( Вход | Регистрация )
OpenGL на Qt4. Это просто! (статья), ознакомительная статья |
registr |
11.1.2011, 11:54
Сообщение
#1
|
Участник Группа: Участник Сообщений: 115 Регистрация: 16.11.2009 Пользователь №: 1234 Спасибо сказали: 8 раз(а) Репутация: 1 |
OpenGL на Qt4. Это просто!
Это ознакомительная статья, посвящённая программированию 3D-графики OpenGL (Open Graphics Library - открытая графическая библиотека) с помощью кроссплатформенной библиотеки Qt4 (Q и Trolltech - красивое название). Я надеюсь, она окажется полезной для тех, кто впервые решил познакомиться с OpenGL и выбирает практичную и удобную библиотеку GUI (Graphical User Interface - графический интерфейс пользователя). Возможности библиотеки Qt далеко выходят за рамки разработки GUI и она считается одной из самых успешных библиотек для разработки кроссплатформенных приложений для различных целей. Конечно же, статья направлена на популяризацию Qt как API (Application Programming Interface - интерфейс прикладного программирования) для создания трёхмерной графики. Поэтому эта статья также окажется полезной для профессиональных программистов OpenGL, незнакомых с возможностями Qt4, и программистов Qt, не работавших с OpenGL. От читателя потребуется знание языка C++. Изложение будет построено таким образом: сначала мы рассмотрим основы вместе с примерами - кусками кода. И в конце будет написана обобщающая всё сказанное программа. Библиотека Qt4 имеет специальный модуль для работы с OpenGL - QtOpenGL. В модуле QtOpenGL содержатся следующие классы: QGLColormap, QGLContext,QGLFormat, QGLFramebufferObject, QGLPixelBuffer, QGLWidget, QWSGLWindowSurface. Классы подключаются стандартным образом:
Рассмотрим класс QGLWidget вместе с наиболее важными функциями-членами и слотами-членами этого класса. Класс:
Конструктор класса:
Функции-члены класса (protected):
Слот-член класса (public):
Более полную и подробную информацию о классе QGLWidget можно найти в справке Qt Assistant, который всегда поставляется с Qt. Аргументом конструктора класса является указатель на объект класса QWidget. Класс QWidget обеспечивает работу с виджетами (интерфейсными элементами окна), а класс QGLWidget унаследован от QWidget и осуществляет связь виджетов и OpenGL. Значение *pwgt=0 указывает, что виджет будет главным окном. Пример:
Функции initializeGL(), resizeGL(), paintGL() являются виртуальными функциями (подробнее о динамическом полиморфизме можно прочитать, например, в книге: Аверкин В.П., Бобровский А.И., Веснич В.В., Радушинский В.Ф., Хомоненко А.Д. "Программирование на C++"). Следовательно, в классе-наследнике Scene3D обозначать функции как virtual ненужно. Функция initializeGL() вызывается только один раз в начале выполнения программы. Судя по названию, в теле этой функции вы можете провести какую-либо инициализацию, т.е. установить некоторые начальные значения. Разумеется, инициализацию можно провести где-то в другом месте, но разумнее инициализацию провести именно в этой функции. И, конечно же, можно изменять первоначальные настройки в любом удобном для этого месте. Пример:
Обратим внимание на первую команду qglClearColor(Qt::white), которая принадлежит классу QGLWidget. В OpenGL она эквивалентна glClearColor(0.0f, 0.0f, 0.0f, 1.0f), которая устанавливает цвет RGBA для очистки окна. Конечно же, вместо qglClearColor(Qt::white) можно использовать glClearColor(0.0f, 0.0f, 0.0f, 1.0f), и выбор определяется удобством и остаётся за вами. Аргументом функции qglClearColor() является значение типа QColor. Это также встроенный класс Qt, как можно догадаться из приставки Q. Особенность его в том, что он соответствует цветам в OS Windows от 0 до 255. Пример:
В соответствии с правилами обозначения в OpenGL значение 1.0f обрабатывается как тип GLfloat (собственный тип OpenGL). Зачем нужны собственные типы? Дело в том, что различные компиляторы и платформы по-разному распределяют память под стандартные типы и программист должен всегда это держать в уме. Собственные типы OpenGL обрабатываются везде одинаково и освобождают нас от этого обременительного занятия. Следующая функция glEnable(GL_DEPTH_TEST) является уже функцией OpenGL, что можно понять из приставки gl в отличие от qgl для первой функции. glEnable(GL_DEPTH_TEST) устанавливает режим проверки глубины объектов, известных также как z-буфер (z-buffer) или буфер глубины. Суть его заключается в том, что каждому пикселю на экране с координатами x и y даётся еще координата z, характеризующая расстояние до наблюдателя. Таким образом, одни объекты могут быть ближе к нам, а другие дальше, а значит, ближние объекты могут закрыть собой дальние. Зачем рисовать дальние объекты, если они невидны? Правильно, незачем! Поэтому нужно иметь метод, чтобы не рисовать скрытые (невидимые) поверхности. Как раз это и выполняет glEnable(GL_DEPTH_TEST). Закомментированная функция glShadeModel(GL_FLAT) отключает режим сглаживания цветов, который всегда установлен по умолчанию. Если вершины имеют разный цвет, то цвет между ними будет плавно переходить из одного в другой. Отключать режим сглаживания нет необходимости, поэтому функция закомментирована и не используется. OpenGL использует следующие простые геометрические построения - примитивы: точка, линия, треугольник (чаще всего), четырёхугольник, многоугольник (реже всего). Например, когда рисуется треугольник, рисуются его внешняя и внутренняя поверхности. Но часто бывает так, что внутренние поверхности рисовать ненужно, так как мы их никогда не увидим. Для этого используется команда glEnable(GL_CULL_FACE) и в результате её исполнения рисуются только внешние поверхности. Как можно догадаться, это повышает быстродействие рисования, так как теперь видеокарта рисует изображение в два раза меньше. Какая поверхность внешняя, а какая внутренняя определяется с помощью последовательности её вершин - закручивание по правилу правого винта (обход вершин по часовой стрелке). Рассмотрим следующую функцию resizeGL(), определенную в Qt. Эта функция вызывается первый раз после initializeGL() и каждый раз автоматически, когда происходит изменение размеров окна. Как вы догадываетесь параметры width и height типа int и есть ширина и высота окна виджета в пикселях, которые отсчитывается от самой верхней левой точки виджета. Эти параметры передаются в функцию resizeGL() автоматически при ее вызове. Очевидно, в её теле требуется задавать настройки, связанные с размером виджета. Пример:
Последовательные команды glMatrixMode(GL_PROJECTION) и glLoadIdentity() загружают единичную матрицу в матрицу проекции. Необходимо знать, что любые преобразования с матрицами не сбрасываются, а аддитивно накапливаются. Поэтому необходимо каждый раз возвращать первоначальные матрицы, в которых не произведено никаких преобразований. С этой цель единичная матрица и загружается в текущую матрицу (проекции), т.к. единичная матрица соответствует матрице без преобразований. Если не отменять преобразования, то следующее преобразование будет совершаться уже относительно предыдущего. Команда 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 автоматически масштабирует всю сцену так, что она становится вытянутой по координате y экрана. Чуть позже в заключительном листинге обобщающей программы будет показано, как этого избежать и "сохранить квадрат квадратным". Также необходимо помнить, что отсчёт координаты y на экране производиться по-разному: из самой нижней точки вверх в OpenGL и из самой верхней точки вниз в экранных координатах. Построение изображения в OpenGL происходит по следующему принципу: (мировые координаты)-->(мировое окно)-->(поле просмотра). Следующая рассматриваемая функция Qt - paintGL() - вызывает рисование сцены и выполняется первый раз после resizeGL() и каждый раз после функции вызова updateGL(). Пример:
Первая закомментированная команда glClear(GL_COLOR_BUFFER_BIT) очищает окно цветом RGBA, который выбран командами qglClearColor(Qt::white) или glClearColor(0.0f, 0.0f, 0.0f, 1.0f) (в данном случае белый цвет). Если в инициализации initializeGL() установлена проверка глубины glEnable(GL_DEPTH_TEST), то можно очистить буфер цвета и глубины вместе glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT). Команды glMatrixMode(GL_MODELVIEW) и glLoadIdentity() загружают единичную матрицу в матрицу моделирования, аналогично как это говорилось выше о матрице проекции. Матрица моделирования отвечает за наблюдение сцены; а матрица проекции за проецирование сцены, которое может быть либо ортогональным, либо перспективным. Далее приведены последовательные преобразования: поворот, трансляция, масштабирование. Значения (GLfloat) xRotate, yRotate, zRotate, xTransl, yTransl, zTransl, xScale, yScale, zScale удобно определить как private данные-члены класса и задать им начальные значения в теле конструктора класса. Важно напомнить ещё раз, что любое преобразование изменяет текущие матрицы. И следующее преобразование осуществляется относительно предыдущего. Поэтому операции (поворот->трансляция) и (трансляция->поворот) приводят к разным результатам; операции (поворот1->поворот2) и (поворот2->поворот1) также могут привести к разным конечным преобразованиям. Все эти преобразования в итоге изменяют матрицу моделирования от единичной. Именно поэтому необходимо при перерисовании сцены загружать единичную матрицу, после чего будут осуществляться указанные преобразования над ней. Теперь можно полностью представить, как происходит долгий путь построения изображения в OpenGL: (мировые координаты)--> ... -->(координаты в окне): (мировые координаты)-->[матрица моделирования]-->(преобразованные координаты)-->[матрица проекции]-->(мировое окно - координаты с отсечением)-->[перспективное преобразование]-->(преобразованные координаты)-->[преобразование поля просмотра]-->(координаты в окне). Последняя функция example_drawAxis() для примера создает оси координат из примитивов-линий. Эту функцию можно определить как функцию-член класса. Пример:
По умолчанию ось y будет направлена вверх в плоскости экрана, ось x вправо в плоскости экрана, а ось z будет перпендикулярна экрану и направлена из него на вас. Оси образуют правую тройку векторов, как и должно быть в стандартной математике. Центр системы координат будет расположен в центре экрана. Эта система координат показывает мировые координаты, которыми задаются трёхмерные объекты в программе. Особенность OpenGL в том, что он работает как машина с настройками (машина состояний, или конечный автомат). Если была установлена некоторая настройка, то она будет выполняться до тех пор, пока не будет изменена. Наглядный пример: команда glColor4f(), которая устанавливает цвет RGBA. Обратите внимание, что совместно использована функция qglColor(). Представление примитивов с помощью команд glBegin() и glEnd() является очень наглядным и простым для понимания, тем не менее, недостаточно эффективным по быстродействию. Поэтому для повышения быстродействия в случаях статической геометрии их часто помещают в дисплейные списки (display lists, имеются также переводы: списки отображения, списки изображения, таблицы отображения). Ещё большей эффективности можно добиться, используя массивы вершин, массивы индексов вершин, массивы цветов вершин и т.д. Этот метод применим также в случае динамической геометрии. Пример использования массивов будет продемонстрирован в заключительном обобщающем листинге программы. Наконец, рассмотрим слот updateGL(). Слоты являются как бы командами и используются Qt в механизме сигналов и слотов, но здесь этот механизм объясняться не будет. Слот updateGL() вызывает выполнение функции paintGL(), которая в свою очередь выполняет рисование сцены. Этот слот можно не определять в своём классе, например, Scene3D, если вы не добавляете в его тело что-то своё. В этом случае он наследуется сам от QGLWidget. Пример:
Собираем всё вместе и не только! Завершающий листинг обобщающей программы. Мы рассмотрели основные функции Qt для работы с OpenGL и сопутствующие команды самого OpenGL. Но для минимального набора знаний нам также потребуется методы обработки нажатия клавиш и событий мыши, определенные в Qt. Всё это будет продемонстрировано в последнем листинге полной программы. А в качестве геометрической фигуры мы нарисуем икосаэдр, используя массив вершин. Программа написана с использованием Qt4.3.0. Листинг состоит из трёх файлов: scene3D.h, scene3D.cpp и main.cpp. scene3D.h
Значение ptrMousePosition типа QPoint хранит координаты на экране: x и y. Они нам понадодятся для определения положения мыши. Класс QPoint находится в модуле QtCore, который здесь явно можно не подключать. В функциях getVertexArray(), getColorArray() и getIndexArray() инициализируются массив вершин VertexArray, массив цветов вершин ColorArray и массив индексов вершин IndexArray. Чуть позже мы поговорим о них подробнее. Функция drawFigure() строит икосаэдр, используя перечисленные массивы. Функции обработки событий мыши mousePressEvent() и mouseMoveEvent() мы будем использовать для вращения сцены. Первая функция вызывается нажатием клавиши мыши, вторая перемещением мыши. Нам нужно знать только, что вторая функция выполняется, когда клавиша мыши нажата - трекинг (отслеживание) выключен. Функцию wheelEvent() - обработка событий колёсика мыши - мы будем использовать для масштабирования сцены. Также приведена функция при отжатии клавиши мыши mouseReleaseEvent(), её удобно использовать, когда нужно определить, какое действие должно выполниться мышью: например, изменение наблюдение сцены или какое-то изменение на сцене. Функция keyPressEvent() вызывается при нажатии клавиши. Все перечисленные функции принадлежат классу QWidget и переопределяются (динамический полиморфизм). scene3D.cpp
В начале программы мы декларируем массивы вершин, цветов и индексов. Чтобы иметь представление, скажу, что икосаэдр имеет 12 вершин и 20 (равносторонних) треугольников. В массив вершин мы запишем декартовы координаты вершин: x, y, z. В массив цветов запишем цвета вершин с тремя составляющими RGB, сгенерировав их псевдослучайно. В массив индексов запишем индексы вершин - три номера (индекса) вершин. Для чего это всё нужно? Чтобы построить какую-нибудь фигуры, мы должны как минимум знать вершины фигуры и знать каким способом из них построить примитивы, например, треугольники. Поэтому в двумерный массив вершин мы заносим координаты вершин, а в массив индексов (фактически массив треугольников) записываем последовательные номера вершин (фактически три последовательные вершины треугольника). Каждой вершине также можно дать цвет - для этого и служит массив цветов. Координаты вершин мы рассчитываем сами, индексы примитива также задаём самостоятельно. Но как вы понимаете, их можно загрузить из файла. А перед этим создать модель в визуальном 3D-графическом редакторе, например в Blender, сохранить модель в файл и, зная структуру файла, сделать загрузчик моделей. В конструкторе класса Scene3D мы задаём начальные значения. Обратите внимание, что мы поворачиваем сцену вокруг оси 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. Вам не составит труда самостоятельно разобраться, что происходит в функциях 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
В главной функции main() мы создаём объект графического интерфейса класса QApplication, который осуществляет контроль и управление приложением. Потом создаём виджет нашего класса Scene3D, задаём его размеры (nWidth, nHeight) и указываем, во-первых, что его нужно изобразить и, во-вторых, при необходимости как его изобразить. Вызов функции exec(), принадлежащей классу QApplication, производит запуск приложения и сопутствующий контроль. В файле-проекте .pro необходимо добавить строку: QT += opengl для сборки приложения совместно с модулем QtOpenGL и системной библиотекой OpenGL. Надеюсь, из этой статьи вы открыли что-то новое и интересное для себя. Я дал вам лишь небольшой начальный импульс, насколько сильно вы его преумножите, зависит от вас. Спасибо за внимание. Для тех, кто незнаком с C++, рекомендую вам следующие книги: 1) Аверкин В.П., Бобровский А.И., Веснич В.В., Радушинский В.Ф., Хомоненко А.Д. "Программирование на C++" (очень хорошая книга для начинающих) 2) Дейтел Х., Дейтел П. "Как прогаммировать на C++" (ОЧЕНЬ большая книга, в ней много всего полезного) Литература по OpenGL: 1) Райт Р.С.-мл., Липчак Б. "OpenGL. Суперкнига" (отличная книга по OpenGL) Литература по Qt4: 1) Qt Assistant и Qt Examples and Demos (помощь и много нужных примеров, поставляются вместе с Qt) 2) Бланшет Ж., Саммерфилд М. "Qt4 .. программирование GUI на C++" 3) Шлее М. "Qt4. Профессиональное программирование на C++" Сообщение отредактировал registr - 15.1.2011, 10:54 |
|
|
Гость_Лена_* |
22.3.2012, 0:39
Сообщение
#2
|
Гости |
Спасибо!!!
|
|
|
Текстовая версия | Сейчас: 23.11.2024, 2:57 |