Gtk, OpenGL и все-все-все

от автора

Не так давно мне пришлось столкнуться с использованием OpenGL в Gtkmm версии 3. В данной статье я постараюсь изложить детали имплементации стандартного виджета для OpenGL-графики.

Gtk::GLArea

Быстрый поиск по фразе Gtkmm+OpenGL выдает кучу примеров для Gtk::GLArea — стандартного виджета для OpenGL. Он имеет довольно гибкий интерфейс, а так же следующие возможности:

  • опциональная поддержка прозрачности самого виджета,
  • настройка буфера трафарета и глубины,
  • сигналы для инициализации и освобождения контекста OpenGL.

В обычном случае, все команды рисования располагаются в обработчике сигнала signal_draw(), но есть возможность создать сцену в процессе инициализации, и она будет отображаться даже при перерисовке окна.

Копаем глубже

Gtk::GLArea содержит некоторое количество опциональных возможностей, которые
будут осложнять изложение, поэтому ссылаться я буду на созданную мной
упрощенную версию — OpenGLWidget.

В целом, жизненный цикл виджета состоит из нескольких этапов:

  • Инициализация
  • Повторяющийся вызов функции отрисовки
  • Утилизация

Рассмотрим более подробно первые два пункта.

Инициализация

За процесс инициализации отвечает метод OpenGLWidget::on_realize, а схема показана на рисунке ниже:

Для перехватывания событий необходимо создать Gdk::Window. Так же наличие
у виджета своего экземпляра окна позволяет рисовать на его поверхности.

Код, который создает Gdk::Window:

GdkWindowAttr attributes; memset(&attributes, 0, sizeof(attributes));  Gtk::Allocation allocation = get_allocation();  //Set initial position and size of the Gdk::Window: attributes.x = allocation.get_x(); attributes.y = allocation.get_y(); attributes.width = allocation.get_width(); attributes.height = allocation.get_height();  attributes.event_mask = get_events (); attributes.window_type = GDK_WINDOW_CHILD; attributes.wclass = GDK_INPUT_OUTPUT;  m_refGdkWindow = Gdk::Window::create(get_parent_window(), &attributes, GDK_WA_X | GDK_WA_Y); set_window(m_refGdkWindow); register_window(m_refGdkWindow);

Здесь задаются атрибуты, такие как тип, класс, позиция, ширина и высота.
Так же важно вызвать Gtk::Widget::set_window, чтобы передать виджету информацию о сконструированном Gdk::Window.
Метод Gtk::Widget::register_window делает возможным получение событий виджетом от указанного окна.

Gdk::GLContext является оберткой над контекстом OpenGL. Его создание иллюстрирует следующий код:

m_Context = m_refGdkWindow->create_gl_context(); if(!m_Context->realize()) {       std::cerr << "Context realize failed" << std::endl;       m_Context.reset();       return; }

Перед выполнением любых команд OpenGL необходимо вызвать Gdk::GLContext::make_current, как это сделано перед инициализацией Framebuffer и Renderbuffer:

m_Context->make_current(); glGenFramebuffersEXT (1, &frame_buffer); glGenRenderbuffersEXT (1, &render_buffer);

Вывод на экран

Здесь необходимо сделать небольшое отступление. Дело в том, что виджет, как таковой, не
имеет непосредственного доступа к внутреннему буферу окна операционной системы и не может в него рисовать.

Поэтому Gtk::GLArea создает связку Framebuffer и Renderbuffer или текстуры.
Все команды OpenGL выполняются над текстурой или Renderbuffer, формируя итоговое
изображение. Далее, вызов функции gdk_cairo_draw_from_gl с необходимыми параметрами
сохраняет его в буфер окна ОС, после оно будет показано пользователю. В OpenGLWidget
используется связка Framebuffer и Renderbuffer.

Последовательность шагов для вывода на экран показана на диаграмме:

Все операции с графикой выполняются в имплементации Gtk::Widget::on_drawOpenGLWidget::on_draw.

Вот код, иллюстрирующий как привязываются буферы:

m_Context->make_current();  auto scale = get_scale_factor ();  m_BufferDimensions = {   scale,   get_allocated_width () * scale,   get_allocated_height () * scale };  glBindRenderbuffer (GL_RENDERBUFFER, render_buffer); glRenderbufferStorage (GL_RENDERBUFFER, GL_RGB8, m_BufferDimensions->width, m_BufferDimensions->height); glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, frame_buffer); glFramebufferRenderbufferEXT (GL_FRAMEBUFFER_EXT,      GL_COLOR_ATTACHMENT0_EXT,     GL_RENDERBUFFER_EXT, render_buffer); glViewport (0, 0, m_BufferDimensions->width, m_BufferDimensions->height );

Здесь необходимо отметить сохранение параметров области вывода в переменную m_BufferDimensions, это нужно для дальнейшего вызова gdk_cairo_draw_from_gl.

Перед вызовом пользовательского колбэка и gdk_cairo_draw_from_gl необходимо проверить статус Framebuffer, и если он не готов, то пропустить эти инструкции. Их выполнение над таким Framebuffer будет ошибочным.

Вызов пользовательского клолбэка осуществляется с помощью механизма сигналов библиотеки sigc:

m_DrawSignal.emit();

Для вывода сформированного изображения из Renderbuffer необходимо выполнить инструкцию:

gdk_cairo_draw_from_gl (cr->cobj(),                                 m_refGdkWindow->gobj(),                                 render_buffer,                                 GL_RENDERBUFFER,                                 m_BufferDimensions->scale, 0, 0,                                  m_BufferDimensions->width,                                 m_BufferDimensions->height);

Заключительный этап — отвязываем буферы:

m_Context->make_current(); glBindRenderbuffer (GL_RENDERBUFFER, 0); glBindFramebufferEXT (GL_FRAMEBUFFER_EXT, 0); m_Context->clear_current();

m_Context->make_current() здесь важен, потому что gdk_cairo_draw_from_gl может поменять текущий контекст.

Handmade Cairo::Context

В заключении рассмотрим вопрос явного, ручного создания Cairo::Context.

Его получает в качестве параметра обработчик сигнала signal_draw() или
метод Gtk::Widget::on_draw. Они вызываются, если есть потребность перерисовать контент виджета.

Создать такой контекст можно функцией Gdk::Window::begin_draw_frame, а в завершении
вызвать Gdk::Window::end_draw_frame.

Пример, как работает эта связка:

void OpenGLWidget::draw_content() {     if(m_refGdkWindow && m_refGdkWindow->is_visible())     {         auto region = m_refGdkWindow->get_visible_region();       /*1*/         auto context = m_refGdkWindow->begin_draw_frame(region);  /*2*/         draw(context->get_cairo_context());                       /*3*/         m_refGdkWindow->end_draw_frame(context);                  /*4*/     } }

Здесь m_refGdkWindow — это Gdk::Window связанный с виджетом. Перед созданием контекста необходимо получить параметры области для рисования (выражение 1). Далее, вызывается Gdk::Window::begin_draw_frame для конструирования Cairo::Context для указанной области (выражение 2). Для формирования контента вызываем Gtk::Widget::draw (выражение 3), которая передаст управление либо имплементации Gtk::Widget::on_draw, либо вызовет обработчик сигнала signal_draw(). Сообщаем о завершении процедуры рисования с помощью Gdk::Window::end_draw_frame
(выражение 4).

ссылка на оригинал статьи https://habr.com/ru/post/559004/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *