Редактирование: Работа с 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 при уничтожении объекта-родителя все его объекты-потомки уничтожаются автоматически. Такая связь эффективна для управления памятью, когда достаточно удалить только верхний в иерархии объект и все связанные с ним нижележащие объекты удалятся автоматически. Здесь важно заметить, что объекты-потомки должны создаваться динамически (через new), иначе может произойти ошибка, связанная с вызовами деструктров. А использование динамических объектов-потомков исключает эту возможную ошибку. Смысл технологии дерева объектов заключается в том, что объект верхнего уровня создаётся нединамически: обычно это виджет окна или виджет главного окна (это и есть объект верхнего уровня). А остальные GUI-объекты создаются динамически как объекты-потомки этого главного виджета (объекта верхнего уровня). Когда объект главного виджета выйдет за область видимости, то всё дерево объектов удалится из памяти. В этом и заключается вся прелесть работы с деревом объектов — не нужно держать в голове динамические объекты GUI и удалять их через delete в деструкторе. В этой части статьи мы не будем создавать дерево объектов и отложим это до второй части. Здесь в обобщающей программе, которая будет приведена во второй половине этой (первой части) статьи, наш объект класса Scene3D будет объектом верхнего уровня. В конечном итоге в конструктор класса QWidget передастся значение 0, что и будет означать, что наш объект класса Scene3D является объектом верхнего уровня в иерархии объектов.
+
В 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 никак не проверяет, какой объект ближе, а какой дальше, он просто последовательно рисует объекты. Если последовательно рисуются: объект1, объект2, то даже если объект2 дальше от наблюдателя, он все равно будет закрывать собой объект1, потому что объект2 нарисовался последним. Но как объяснить OpenGL, какой объект ближе, а какой дальше? На помощь нам приходит технология отсечения скрытых (т.е. невидимых) поверхностей с помощью буфера глубины, известного также как z-буфер. Суть этой технологии состоит в том, что каждому пикселю объекта (например, треугольнику) на экране с координатами x и y даётся еще координата z, называемая глубиной и характеризующая расстояние до наблюдателя. Задача сводится к тому, чтобы не рисовать «закрытые» пиксели объектов. Если сохранить глубину пикселей объекта1 в буфер глубины и потом сравнить с глубиной пикселей объекта2, то можно определить, какие пиксели объектов видны на экране, а какие скрыты. Например, рассмотрим такую ситуацию: последовательно рисуются два треугольника. Когда обрабатывается первый треугольник, то значения глубины его пикселей сохраняются в буфер глубины. Затем когда обрабатывается второй треугольник, то глубина уже его пикселей сравнивается со значениями в буфере глубины, и те пиксели, которые ближе к нам, замещают те, которые дальше. Команда glEnable(GL_DEPTH_TEST) включает режим, при котором проводится сравнение глубины пикселей объектов с глубиной, хранящейся в буфере глубины. Также нужно будет сообщить системе об использовании буфера глубины, т.е. нужно подключить сам буфер глубины. Qt автоматически в конструкторе класса QGLWidget по умолчанию задаст такой формат в контекст, т.е. использовать (подключить) буфер глубины. Поэтому буфер глубины можно явно и не подключать, он подключится сам. Использование буфера глубины и сравнение глубины являются эффективным способом удаления (отсечения) невидимых поверхностей.  
+
Следующая функция 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); // создаём приложение, инициализация оконной системы
-
   Scene3D scene1; // создаём виджет класса Scene3D
+
   scene1 = new Scene3D; // создаём виджет класса Scene3D
-
   scene1.setWindowTitle("lecture1"); // название окна   
+
   scene1->setWindowTitle("lecture1"); // название окна   
-
   scene1.resize(500, 500);  // размеры (nWidth, nHeight) окна   
+
   scene1->resize(500, 500);  // размеры (nWidth, nHeight) окна   
-
   scene1.show(); // изобразить виджет
+
   scene1->show(); // изобразить виджет
-
   // scene1.showFullScreen();
+
   // scene1->showFullScreen();
-
   // scene1.showMaximized();   
+
   // 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 необходимо добавить следующую строку:

Пожалуйста, обратите внимание, что все ваши добавления могут быть отредактированы или удалены другими участниками. Если вы не хотите, чтобы кто-либо изменял ваши тексты, не помещайте их сюда.
Вы также подтверждаете, что являетесь автором вносимых дополнений, или скопировали их из источника, допускающего свободное распространение и изменение своего содержимого (см. Wiki.crossplatform.ru:Авторское право). НЕ РАЗМЕЩАЙТЕ БЕЗ РАЗРЕШЕНИЯ ОХРАНЯЕМЫЕ АВТОРСКИМ ПРАВОМ МАТЕРИАЛЫ!