Давным давно, когда я писал игру под адроид, нужно было мне разобраться с OpenGL ES 1.1. Вроде бы ничего сложного, 2D графика, нужно было просто рисовать PNG изображения, с возможностью масштабирования, поворота, добавления прозрачности. Тогда я потратил на это около недели, а может даже и больше, уже не помню. Было сложно, поскольку с OpenGL я никогда дела не имел. Сильно помог исходный код libgdx, в котором, кстати, все низкоуровневые OpenGL функции спрятаны от разработчика.
Времена меняются, на смену OpenGL ES 1.1 приходит версия 2.0, которая довольно сильно отличается. Приходится разбираться, что это за шейдеры, и почему без них никак. На это опять уходит несколько дней. Казалось бы, должно быть легко, ведь 2D, все просто. Например, если использовать QML, это делается вот так:
Image
{
source: "brick.png"
opacity: 0.8
rotation: 90
}
А если писать все на С++, то получается много-много строк кода, которые сложно понять, если не знаком с OpenGL. Я пытался найти какую-нибудь библиотеку, обертку над OpenGL, как libgdx, только для Qt, но безуспешно. Поэтому решил, после того, как у меня все заработает, я напишу небольшую обертку, которая прячет все OpenGL вызовы и позволяет удобно работать с 2D графикой.
Код обертки на GitHub
Пример использования на GitHub
Для того, чтобы использовать обертку в своем приложении, необходимо добавить одну строчку в .pro файл:
include (opengl-qt/opengl-qt.pri)
Унаследовать окно приложения от OpenGLQt::Widget:
class Widget : public OpenGLQt::Widget
{
public:
Widget();
protected:
virtual void drawSprites();
};
Передать в конструкторе путь к файлу и количество изображений в нем. Запустить таймер:
Widget::Widget()
: OpenGLQt::Widget(10, "spritesheet.png")
, m_counter(0)
{
startTimer();
}
Переопределить метод drawSprites():
void Widget::drawSprites()
{
static Sprite apple = {86, 118, 30, 30}; // участок изображения, который нужно нарисовать
static Color color = {1, 1, 1, 1}; // оттенок цвета
static DrawSpriteArgs args;
args.sprite = apple;
args.color = color;
args.x = 10; // центр изображения по оси х
args.y = 15; // и по оси у
args.angle = 3.14f / 2.0f; // поворот на 90 против часовой стрелки
args.scaleX = 0.5f; // уменьшение на 50% по оси х
args.scaleY = -1.0f; // зеркальное отражение по оси у
drawSprite(args);
}
И все. Довольно легко, правда? Если вы хотите использовать эту обертку, вам нужно будет вместить всю свою графику в один PNG файл. Это можно сделать, например, с помощью TexturePacker. Смысл этого в увеличении быстродействия: текстура загружается в видеопамять один раз, при старте, и остается там до конца работы программы. Когда вы хотите нарисовать изображение, вам нужно указать его координаты в этой текстуре.
Немного OpenGL подробностей. Vertex shader:
uniform mat4 mvp_matrix;
attribute vec4 a_position;
attribute vec2 a_texcoord;
attribute vec4 a_color;
varying vec4 v_color;
varying vec2 v_texcoord;
void main()
{
gl_Position = mvp_matrix * a_position;
v_texcoord = a_texcoord;
v_color = a_color;
}
Fragment shader:
#ifdef GL_ES
#define LOWP lowp
precision mediump float;
#else
#define LOWP
#endif
varying vec2 v_texcoord;
varying LOWP vec4 v_color;
uniform sampler2D texture;
void main()
{
gl_FragColor = v_color * texture2D(texture, v_texcoord);
}
Большинство OpenGL функциий вызывается один раз при инициализации программы. После этого запускается таймер, который 60 раз в секунду вызывает обновление экрана:
void Widget::startTimer()
{
m_timer.start(1000.0f / 60.0f, this);
}
void Widget::timerEvent(QTimerEvent *)
{
updateGL();
}
void Widget::paintGL()
{
beginDraw();
drawSprites();
endDraw();
}
void Widget::beginDraw()
{
m_idx = 0;
glClear(GL_COLOR_BUFFER_BIT);
glClearColor(m_background.r,
m_background.g,
m_background.b,
m_background.a);
}
void Widget::endDraw()
{
glDrawArrays(GL_TRIANGLES, 0, m_idx / 2);
}
Все рисование происходит в методе drawSprites(), пример которого приведен выше. Оно заключается в заполнении структуры DrawSpriteArgs и вызове метода drawSprite для каждого изображения, которое необходимо нарисовать. Если хочется чего-то нестандартного, например, вращать изображение не вокруг центра, можно вручную заполнять массивы координат, текстур, и цветов. Максимальный размер массивов необходимо задать при старте программы в конструкторе, потому что QVector при увеличении может переехать на другой участок памяти, и нужно будет оповестить об этом OpenGL:
int vertexLocation = m_program->attributeLocation("a_position");
m_program->enableAttributeArray(vertexLocation);
glVertexAttribPointer(vertexLocation, 2, GL_FLOAT, GL_FALSE, 0, &m_verts[0]);
int texcoordLocation = m_program->attributeLocation("a_texcoord");
m_program->enableAttributeArray(texcoordLocation);
glVertexAttribPointer(texcoordLocation, 2, GL_FLOAT, GL_FALSE, 0, &m_texCoords[0]);
int colorLocation = m_program->attributeLocation("a_color");
m_program->enableAttributeArray(colorLocation);
glVertexAttribPointer(colorLocation, 4, GL_FLOAT, GL_FALSE, 0, &m_colors[0]);
Проекционная матрица была взята отсюда. Она устанавливается во время изменения размеров окна:
void Widget::resizeGL(int w, int h)
{
glViewport(0, 0, w, h);
QMatrix4x4 mat(
2.0f/w, 0, 0, -1,
0, 2.0f/h, 0, -1,
0, 0, -1, 0,
0, 0, 0, 1
);
m_program->setUniformValue("mvp_matrix", mat);
}
Вот пожалуй и все. Тем, кто действительно захочет использовать обертку, очень советую скомпилировать пример и разобраться, как он работает. Код был протестирован на Windows 7 (Qt 4.8 MinGW, MSVC 2008), Mac OS X 10.8 (Qt 5.1 Clang), Nokia N9 (Qt 4.7), BlackBerry 10 (Qt 4.8), везде выполняется корректно.
Заключение
Я думаю, что не только у меня возникали подобные проблемы и надеюсь, что эта статья поможет кому-нибудь, что эта маленькая оберка станет кирпичиком в каком-нибудь проекте и облегчит кому-то жизнь. А может быть кто-то просто подчерпнет себе что-то из кода — пожалуйста. Поскольку я не являюсь специалистом по OpenGL, не исключено что в реализации присутствуют ошибки. Если у вас есть исправления или предложения по улучшению, не стесняйтесь, пишите в комментариях.
Автор: beemaster