OpenGL контекст
Ранее, мы создали окно используя
winit
и научились обрабатывать события окна.
В этой главе мы продолжим изучать OpengGLES, подключим OpenGL контекст в нашему окну и покрасим его в синий цвет.
OpenGL
Если вы будете искать OpenGL на crates.io, вы сможете найти мноцество библиотек которые занимаются взаимодействием с OpenGL, такие как glium
, glitter
и может быть другие.
Цель этой книги, показать вам детали взаимодействия с OpenGL на более низком уровне. Это не говорит что всю работу мы будем делать самостоятельно!
gl и gl_generator
Краеугольный камень многих проектов OpenGL более высокого уровня в том что они содержат три пакета:
khronos_api
- этот пакет содержит файлы XML для различных API-интерфейсов Khronos, таких как EGL, WGL, GLX, GL и другие.gl_generator
- это инструмент для генерации привязок кода Rust из XML-файловkhronos_api
. Думайте об этом как о своем собственном настраиваемом генераторе для загрузки OpenGL (что-то вроде GLEW).gl
- это API OpenGL, уже созданный с использованиемgl_generator
. Это почти правильно. На деле,gl
код генерируется каждый раз когда вы собираете свой проект. Более детально мы обсудим это позже.
Как я уже сказал, наша задача изучить OpenGL ES с использованием Rust на достаточно низком уровне. Но это воовсе не означает что нужно использовать эти низкоуровневые библиотеки, хоть некоторые из них мы и будем использовать. Однако я преверженнец достаточно чистого кода, и поэтому мы будем использовать разные из них. При поиске низкоуровневых библиотек для работы c OpenGL я натыкался на библиотеки обвязки - wrappers
, которые просто дают доступ к API, но так как это не входит в их задачи, они не заботятся об удобстве использования.
Библиотека khronos-egl
EGL управляет графическим контекстом, привязкой поверхностей/буфер, синхронизацией рендеринга и обеспечивает "высокопроизводительный, ускоренный, смешанный режим 2D и 3D рендеринга с использованием других API Khronos".
Давайте добавим пакет khronos-egl
в наш проект в секцию [dependencies]
в файл Cargo.toml
:
Cargo.toml, incomplete
[dependencies]
egl = {package = "khronos-egl", version = "4.1", features = ["dynamic"] }
Нам буlет удобнее общаться с EGL с помошью короткого имени egl
, поэтому мы сделали ссылку на пакет khronos-egl
, указали версию и свойства которые позволяют загружать во время исполнения динамическую библиотеку libEGL.so
.
Чуть ниже нам потребуется Arc, для того чтобы безопасно передавать между потоками EGL структуру.
Поэтому добавим следующие строчки в начало нашего main.rs
файла:
#![allow(unused)] fn main() { use std::sync::Arc; }
Также инициализируем наш EGL объект, добавив в начало функции main
следующий код:
fn main() { // EGL setup here let egl = unsafe { Arc::new( egl::DynamicInstance::<egl::EGL1_4>::load_required() .expect("unable to load libEGL.so"), ) }; // Setup OpenGL ES API egl.bind_api(egl::OPENGL_ES_API) .expect("unable to select OpenGL ES API"); // for OpenGL ES ...
Давайте разберем на крупицы код, который мы добавили.
Первой конструкцией мы создали объект EGL, попытавшись во время исполнения программы загрузить динамическую библиотеку libEGL.so
(для Linux).
Далее мы указали этой библиотеке какой уровень АПИ мы будем использовать. В нашем случае мы передали аргумент egl::OPENGL_ES_API
в функцию bind_api
. Также возможно туда передать константу egl::OPENGL_API
, но это не входит в задачи этой книги.
Просмотр документации по зависимости локально
Мы можем попросить cargo
сформировать и открыть документацию на любой пакет, от которого зависит наш проект:
cargo doc -p khronos-egl --no-deps --open
-p khronos-egl
- это сокращение для--package khronos-egl
, указывает, что нам нужна документация для этого пакета--no-deps
отключает рекурсивную документацию для всех зависимостей khronos-egl. Мы хотим отключить это в Windows, потому чтоgl
транзитивно зависит от других пакетов, и создание документации для них заняло бы слишком много времени.--open
открывает сгенерированную документацию в окне браузера.
Создание графического контекста
После того как вы указали какой API вы будете использовать совместно с EGL время создать графический контекст.
Для начала нам нужна будет информация о нашем дисплее. Мы не будем настраивать его, вместо этого просто получим информацию о дисплее по умолчанию и инициализируем его. Добавьте следующий код после вызова egl.bind_api
:
#![allow(unused)] fn main() { // Setup Display let display = egl .get_display(egl::DEFAULT_DISPLAY) .expect("unable to get display"); egl.initialize(display).expect("unable to init EGL"); }
Чтобы работать с графическим контекстом нам потребуется настроить его. В данном случае параметрами дисплея являются аттрибуты графического буфера. Более подробно значение каждого из атрибутов мы рассмотрим в следующих главах. А сейчас просто объявим их добавив следующий код:
#![allow(unused)] fn main() { // Create context let attrib_list = [ egl::BUFFER_SIZE, 16, egl::DEPTH_SIZE, 16, egl::STENCIL_SIZE, 0, egl::SURFACE_TYPE, egl::WINDOW_BIT, egl::NONE, ]; }
Мы объявили достаточно простые атрибуты нашего графического буфера, но это не говорит что система может их нам предоставить. Так что давайте запросим у графической подсистемы конфигурацию которая будет соответствовать этим атрибутам. Добавляем следующие строчки:
#![allow(unused)] fn main() { // Get the matching configuration. let config = egl .choose_first_config(display, &attrib_list) .expect("unable to choose EGL configuration") .expect("no EGL configuration found"); }
Теперь у нас есть конфигурация графического буфера, совместимого с запрошенными нами атрибутами. Самое время создать графический контекст. У контекста как и у графического буфера также есть свой набор аттрибутов. Как и на что они влияют, мы рассмотрим в следующих главах. Сейчас же мы создадим графический контекст по умолчанию. То есть с пустым набором аттрибутов. Добавим ниже следующий строчки:
#![allow(unused)] fn main() { let ctx_attribs = [egl::NONE]; let ctx = egl .create_context(display, config, None, &ctx_attribs) .expect("unable to create EGL context"); }
Создание EGL поверхности
В этой главе мы уже создали графический контекст, но этот графический контекст ничего не знает об окне в котором он должен быть отображен. Для того чтобы отобразить нашу графику в окне нам нужно будет привязать графический контекст к окну. И так чтобы создать поверхность окна, нам потребуется графический контекст, ссылка на окно и экран. В предыдущей главе мы создали окно с помошью winit
:
#![allow(unused)] fn main() { let _window = wb.build(&event_loop).unwrap(); }
Указатель на окно
Для того чтобы передать ссылку на окно, нам потребуется сырой (raw
) указатель на него. То есть нам потребуется платформо зависимимый указатель на оконной системы. В случае с Linux это указатель на X11 или Wayland окно. Уже немного сложнее.
Для того чтобы удобно работать с сущностями оконной системы в Rust существует пакет raw-window-handle
, который также является зависимостью для winit
. Добавим его в наш проект:
> cargo add raw-window-handle
И укажем что бы будем использовать сущности из этого пакета, добавив в начало нашего main.rs
файла:
#![allow(unused)] fn main() { use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; }
Так как работа с сырыми указателями не является безопасной в идеологии Rust, то код создания поверхности будет помечен ключевым словом unsafe
. Однако не стоит относится к unsafe
как к монстру, потому как это всего лишь контракт между компилятором и программистом, где программист берет на себя ответственность за поведение программы. И да, возможно в некоторых ситуациях программа может завершиться не корректно.
Изменим строчку создания окна, перименовав переменную _window
на window
и добавим код создания поверхности.
#![allow(unused)] fn main() { ... let window = wb.build(&event_loop).unwrap(); // Create a EGL surface let surface = unsafe { let window_handle = match window.raw_window_handle() { RawWindowHandle::Xlib(handle) => { handle.window as egl::NativeWindowType } RawWindowHandle::Xcb(handle) => { handle.window as egl::NativeWindowType } RawWindowHandle::Wayland(handle) => { handle.surface as egl::NativeWindowType } _ => { panic!("Other handle type"); } }; egl.create_window_surface(display, config, window_handle, None) .expect("unable to create an EGL surface") }; }
Разберемся с этим кодом. Как я уже упамянул выше, код создания поверхности помечен ключевым словом unsafe
и блок кода в фигурных скобках оканчивается ;
. Это говорит компилятору о том что весь этот блок является выражением, которое возвращает значение. Чтобы было более понятнее о конструкции unsafe
и так как я изначально пишу эту главу на Linux, то я не обрабатывал другие оконные системы других операционные систем. Поэтому на Linux этот код будет нормально, однако он завершится паникой на OSX, Windows и других системах. Обработку других операционных систем будет добавлена по мере написания книги.
Итак мы получили указатель на окно и привели его к совместимому типу egl::NativeWindowType
из пакета khronos-egl
. Последней строчкой мы вызвали код создания поверхности которая будет ассоциирована с дисплеем и нашим окном, в соответствии с конфигурацией.
Остался последний шаг перед использованием OpenGL ES. Присоединить EGL контекст к поверхностям. Добавьте чуть ниже одну строчку:
#![allow(unused)] fn main() { egl.make_current(display, Some(surface), Some(surface), Some(ctx)) .expect("unable to bind the context"); }
Вся подготовительная работа завершена. Теперь мы можем использовать gl
функции!
Заполним цветом окно
Для работы с gl
функциями нам потребуется пакет opengles
. При выборе пакета я руководствовался в первую очередь поставленными перед этой книгой задачами. Есть также другие пакеты обертки над OpenGL и OpenGL ES. Но часть из них имеет более высо-уровневый АПИ, который не дас достаточно четкого понимания работы с OpenGL. Другая часть является действительно библиотеками обертками, при использовании которых утонули бы в unsafe
конструкциях тем самым затруднив понимание деталей взаимодействия с OpenGL.
Чтобы добавить пакет opengles
в проект введите команду в терминале из корня проекта:
> cargo add opengles
Также добавим в начало файла main.rs
строчку которая позволит использовать GL вызовы с короткими именами. Я могу предположить что автор пакета opengles
планировал также добавить поддержку других версий OpenGL ES.
#![allow(unused)] fn main() { use opengles::glesv2 as gl; }
Перед входом в цикл обработки событий окна выполним еще несколько команд, которые при первом запуске очистят содержимое окна. Для этого нам нужно будет установить цвет которым мы будем заполнять наше окно во время очистки, очистить EGL поверхность и перенаправив буфер цвета в окно.
#![allow(unused)] fn main() { gl::clear_color(0.3, 0.3, 0.5, 1.0); gl::clear(gl::GL_COLOR_BUFFER_BIT); egl.swap_buffers(display, surface) .expect("unable to post EGL context"); }
Самое время запустить наш код!
> cargo run
Если все собралось без ошибок, то вы увидите окно заполненное синим цветом.
Если вы попробуете перемещать любое другое окно поверх окна нашего приложения, то вы заметите что наше окно не обновляется. Я надеюсь вам не составит больших усилий исправить это.
Возможно, сейчас вы подумаете о том что "мы написали такое большое количество кода и получили всего-лишь синее окно". Когда я писал OpenGL-приложение в первый раз в жизни, я тоже был смущен этим, и даже бросил на какое-то время OpenGL.
Важно понимать что в этой главе мы подгтовили фундамент для дальнейшей работы c OpenGL. Проведя небольшой рефакторинг, и распределив сервисный код по модулям вашего приложения, он станет более лаконичным.
Код этой главы доступен в основном репозитории книги.
Далее, длинный и "современный" способ нарисовать треугольник на экране.