Предисловие
Об Авторе
Я специалист в области компьютерных технологий с 25+ летним опытом. В основном я занимаюсь программированием на всех востребованных рынком языках.
Я начал программировать в 18. Увидев мой интерес к компьютерам, мои замечательные родители, купили мне ZX Spectrum. Вдоволь наигравшись в компьютерные игры, я понял что нужно что-то делать. И когда в одном из фильмов я увидел световое перо, у меня возникла идея собрать его самостоятельно. Ведь у меня начальное художественное образование.
Я попросил свою маму принести мне с работы фоторезистор.
И вот у меня в руках оптопара в металлическом корпусе... Но напильник решает все. И когда крышка элемента была сточена, осталось дело за кодом. Идея заключалась в том чтобы гонять по экрану телевизора точку, и слушать порт к которому была подключена оптопара.
Первым языком я изучил Бэйсик. И первую программу я написал на нем.
Но это была моя первая настоящая программа на Ассемблере. Код работал идеально, но кто-же знал что "черный экран Электронной Лучевой Трубки испускает чрезмерное излучение для оптопары".
Так я стал программистом и узнал что баги бывают не только в коде
Windows был очевидным выбором для хомячка в конце 90-х. Помните фильм "Хакеры" с Анджелиной Джоли. И если внимательно смотреть то можно увидеть консоль, SSH-сессию и всякое такое.
Вот вы в конце 90-х часто коннектились по SSH к удаленному серверу? Ну согласитесь же интересно?
Вообщем я поджал раздел файловой системы Windows и начал свой путь в Linux. Чего только я не перепробовал - RedHat, Fedora, Mandriva, Debian, FreeBSD, OpenBSD и другие, включая Solaris. Знакомство с UNIX кончилось инициативой - "не загружать Windows в течении месяца, и потом принять решение".
Хватит знакомится, пора использовать по максимуму
Вообщем с 2004-го года у меня только Gentoo. Вот как поставил в 2004-м, так и работает.
Gentoo - это про производительность. Когда ты из своего железа выжимаешь максимум, получая максимальную гибкость системы.
Когда появилась Nodejs - это было круто
Писать на одном языке и клиент и сервер, и при этом это серьезно превосходит по производительности все остальное. У меня были отличные проекты на Nodejs и Mysql, несколько сайтов, система морфологического анализа, я писал свои расширения для ноды (yandex-money...) и делал пулреквесты в существующие (libxmljs...).
И это всё - тоже про производительность. Тебе не надо изучать несколько языков, пиши только на JavaScript.
Что еще рассказать, историй хватит на небольшую книжку.
Мне нравится изучать все новое
-
Однажды пришлось освоить разработку под Android (SDK+NDK), потом был iOS (ObjC/Swift), MacOSX (ObjC/Swift).
-
Помню занимался Digital Signal Processing (DSP), нужно было речь человека превратить в мяукание.
-
Занимался Natural Language Processing (NLP), портировал свой модуль морфологии с Nodejs на Golang.
-
Хотел позаниматься SEO-оптимизацией контента. RAKE - хороший алгоритм, но перфекционизм взял свое и я посчитал TF/IDF всей Википедии на 6 языках.
Когда считал TF/IDF, то возникли проблемы с памятью, Go не эффективно ее высвобождал.
Поэтому я изучил Rust
Я считаю этот язык одним из самых важных. Если представить его как язык для общения, то Rust - наверное один из самых выразительных языков в мире, такой же как Русский в контексте мировой литературы.
Rust дает уверенность в качестве кода, при этом позволет получить максимальную производительность и стабильность в боевых условиях. Я не могу сказать что написав код на C/C++ вы создадите что-то хуже чем на Rust. Но я также не могу сказать, что вы сделаете это лучше.
Сейчас в зону моих интересов также входит блокчейн, Deep Learning, OpenCL и Cuda.
Пожелания от автора
Надеюсь, что моя инициатива собрать опыт по оптимизации приложений, профилированию производительности и тестированию, будет вами воспринята как хорошая база для своих исследований и вы получите собственный опыт в создании приложений, которыми вы смогли бы гордится по прошествии времени. И вам будет что показать миру, кроме фиги.
Спасибо вам огромное за прочтение этой книги. Надеюсь время которое Вы потратите не будет потрачено зря и вы получите удовольствие. Я же постараюсь сделать текст максимально читабельным и веселым, чтобы вы не скучали как при прочтении документации, чем вы обычно занимаетесь.
Помимо этого я хотел бы надеятся на поддержку сообщества, потому как я не считаю это каким-то произведением искусства или чем-то личным, и любые коментарии и обсуждения будут восприняты очень доброжелательно. Этот труд скорее компиляция идей и опыта сообщества заинтересованных людей.
Однако я всетаки оставляю за собой право модерировать этот репозиторий на github и сообщество в Reddit.
Также вам нужно знать о принятой мной лицензии CC0 1.0 Universal на этот материал. Дополнительный код конечных продуктов находится под лицензией GNU General Public License v3.0 в ветке main
.
С Уважением, Дудочкин Виктор.
OpenGL ES 2.0
TODO:
Настройка с нуля
Привет! Давайте узнаем, как работать с OpenGL ES в Rust.
Я назвал эту главу "Настройка с нуля", потому что предполагаю, что я мало знаю Rust и базовые знания 3D-графики и OpenGL ES.
Таким образом, это руководство может научить вас основам Rust и тому, как заставить Rust работать с OpenGL, однако для более глубокого изучения OpenGL вам понадобится другой учебник или книга.
"С нуля" также означает, что мы будем пытаться создавать абстракции самостоятельно, чтобы лучше узнать Rust. В дополнение к этому, мы сможем следовать существующим руководствам по OpenGL, потому что мы будем точно знать, какие функции OpenGL мы вызываем.
Обратите внимание, что я тоже учусь, делая это. Тем не менее, я уже однажды написал немного беспорядочный рендерер OpenGL ES на Rust, поэтому у меня есть некоторое представление о том, как это должно происходить. Я также не тороплюсь, чтобы убрать беспорядочный код и максимально упростить конечный результат.
Настройка для разработки на Rust
Есть много способов настроить среду разработки для Rust. Вы можете выбрать наиболее удобную для вас настройку на этой веб-странице. Я объясню свою установку, которой вы можете следовать.
Поскольку Rust изначально разрабатывался как кроссплатформенный, чистый код Rust будет компилироваться и запускаться на многих платформах. Мы будем использовать подход, который не заставит Вас погружаться в детали взаимодействия с библиотеками написанными на C.
Я пишу это руководство используя Linux в качестве основной платформы, и если вы используете OSX, то использование этого руководства не потребует практически никаких усилий.
Во-первых, для любой настройки потребуется rustup, установщик набора инструментов Rust, который позаботится об обновлении Rust и многое другое. Если вы установили Rust с помощью диспетчера пакетов вашей ОС или Homebrew, я рекомендую удалить его и переустановить через rustup
.
По завершении rustup должен быть доступен из командной строки:
> rustup --version
rustup 1.24.2 (755e2b07e 2021-05-12)
Установите набор инструментов с помощью rustup (возможно, уже установлен):
Установка в Linux
> rustup install stable-x86_64-unknown-linux-gnu
Установка в OSX
> rustup install stable-x86_64-apple-darwin
Установка в Windows
В Windows Rust доступен с двумя наборами инструментов: GNU (совместим с библиотеками Mingw C) и MSVC (совместим с библиотеками Microsoft C ++ C). Мы будем использовать набор инструментов MSVC.
> rustup install stable-x86_64-pc-windows-msvc
Сделать по умолчанию:
> rustup default stable
rustc
and cargo
оба должны работать (настройте требуемые пути к среде и снова войдите в систему, если они не работают):
> rustc --version
rustc 1.52.1 (9bc8c42bb 2021-05-09)
> cargo --version
cargo 1.52.0 (69767412a 2021-04-21)
Я использую бесплатную Visual Studio Code, потому что как она имеет очень хорошую поддержку Rust за счет использования расширений.
Привет мир
В командной строке создайте новый проект Rust:
> cargo new --bin new-project
Created binary (application) `new-project` project
Вы можете запустить его из командной строки:
> cd new-project
new-project> cargo run
Compiling new-project v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 1.8 secs
Running `target\debug\new-project`
Hello, world!
Похоже, "Hello world" уже написано, как это скучно?
Запустите VSCode и откройте тот же проект Rust. Он будет содержать каталог src с файлом main.rs. В файле main.rs
вы найдете функцию main
:
fn main() { println!("Hello, world!"); }
Поздравляю! Мы готовы начать Cоздание окна.
Окно
Ранее Мы получили некоторую мотивацию для использования Rust и настроили нашу среду разработки.
В этой главе мы создадим окно! Если это не звучит слишком увлекательно, мы также узнаем о пакетах Rust. Очень волнующе.
Чтобы создать окно, которое работает на нескольких платформах, а также использовать OpenGL ES контекст или кросс-платформенный ввод пользователя мы будем использовать пакет winit.
Библиотеки Rust
Библиотеки Rust называются crates
, а консольный менеджер пакетов для Rust называется cargo
. Библиотеки могут быть найдены на центральном репозитории по ссылке crates.io.
Новуй пакет для Rust можно создать в используя команду cargo new library-name
. Структура каталогов для новой пакета будет выглядеть очень похожэ на наш первый проект hello-world
:
library-name
src
lib.rs
Cargo.toml
Разница от исполняемого проекта, в том что в директории src
вместо main.rs
присутствует lib.rs
.
Файл Cargo.toml
описывает наш проект, в независимоти от того библиотека это, или исполняемый проект. Он содержит идентификатор преокта, список зависимостей, ссылку на документацию и многое другое. За более детальной информацией по содержимому манифеста Cargo.toml
обратитесь к документации по Cargo.
Пакет winit
Пакет winit
это кросс-платформенная библиотека для создания окна и обработки событий пользователя, таких как ввод пользователя с клавиатуры или мышки.
Можно посмотерть более детальную информацию об этом пакете по ссылке winit.
Зависимости
Для того чтобы быстро довавлять зависимости в наш проект мы будем использовать cargo-edit
. Для этого нужно его установить с помошью команды в вашем терминале:
> cargo install cargo-edit
Давайте создадим новый проект по имени game
.
> cargo new --bin game
Чтобы добавить пакет winit
в наш проект введите команду в терминале из корня проекта:
> cargo add winit
Эта команда добавит секцию [dependencies]
в файл Cargo.toml
, с пакетом winit
в качестве зависимости:
Cargo.toml, incomplete
[dependencies]
winit = "0.25.0"
На момент написания этой главы новейшей версий winit
является 0.25.0
. Вы можете ввести эту версию, чтобы убедиться, что все компилируется.
Указание зависимости позволяет загрузить её.
Чуть ниже я объясню назначение остальных зависимостей, сейчас же приведите секцию [dependencies]
к следующему виду:
[dependencies]
env_logger = "0.8"
khronos-egl = "4.1"
log = "0.4"
opengles = "0.1"
raw-window-handle = "0.3"
winit = "0.25"
Чтобы использовать заши зависимости, мы должны ссылаться на них.
Использование winit
Укажите ссылки на неообходимые нам структуры, добавив в верхнюю часть файла main.rs
соответствующий код:
#![allow(unused)] fn main() { use winit::{ dpi::{LogicalSize, PhysicalSize, Size}, event::{Event, KeyboardInput, VirtualKeyCode, WindowEvent}, event_loop::{ControlFlow, EventLoop}, window::WindowBuilder, }; }
Этот позволит использовать короткие имена структур из пакета winit
. Функции winit
могут быть вызваны как EventLoop::new()
.
Инициализация winit.
Приведите код функции main
к виду показаному ниже:
fn main() { let event_loop = EventLoop::new(); let wb = WindowBuilder::new() .with_min_inner_size(Size::Logical(LogicalSize::new(64.0, 64.0))) .with_inner_size(Size::Physical(PhysicalSize::new(900, 700))) .with_title("Game".to_string()); let _window = wb.build(&event_loop).unwrap(); event_loop.run(move |event, _, control_flow| { *control_flow = ControlFlow::Wait; match event { Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::KeyboardInput { input: KeyboardInput { virtual_keycode: Some(VirtualKeyCode::Escape), .. }, .. } => *control_flow = ControlFlow::Exit, WindowEvent::Resized(_) => { // make changes based on window size } _ => {} }, Event::RedrawEventsCleared => { // render window contents here } _ => {} } }); }
Ниже мы обсудим каждую деталь кода. Но сначала запустим!
> cargo run
Возможно вы не сразу заметите созданное окно, потому что мы ничего не отображали в нем.
Compiling lesson-01-window v0.1.0 (/home/opengles-tutorial)
Finished dev [unoptimized + debuginfo] target(s) in 9.31s
Running `/home/opengles-tutorial/target/debug/lesson-01-window`
Прервите исполнение с помощью комбинации клавишь Ctrl+C
.
Разбор кода
Вернемся к коду. Давайте разберем его по крупицам.
#![allow(unused)] fn main() { let event_loop = EventLoop::new(); }
Эта строчка инициализирует цикл событий для обработки событий вашей оконной системы, таких как запрос на перерисовку окна или ввод пользователя. До тех пор пока работает цикл обработки событий работает и ваше приложение.
#![allow(unused)] fn main() { let wb = WindowBuilder::new() .with_min_inner_size(Size::Logical(LogicalSize::new(64.0, 64.0))) .with_inner_size(Size::Physical(PhysicalSize::new(900, 700))) .with_title("Game".to_string()); }
Строчки выше конфигурируют окно используя Builder паттерн. Мы задаем минимальный рамер окна, текущий размер окна и заголовок.
#![allow(unused)] fn main() { let _window = wb.build(&event_loop).unwrap(); }
Мы создаем окно, привязав его к циклу обработки событий оконной системы. Мы использовали имя переменной с префиксом поддеркивания _window
, чтобы избежать предупреждающих сообщений компилятора, потому как мы еще не используем методы окна.
#![allow(unused)] fn main() { event_loop.run(move |event, _, control_flow| { ... }); }
Конструкция выше запускает основной цикл обработки событий оконной системы. Программа работает до тех пор пока мы находимся в этом цикле. Единственным аргументом этого цикла является обработчик событий в виде замыкания.
Рассмотрим процесс обработки событий более детально. Обработчик событий помимо непосредственно события получает ссылку на объект, который контролирует цикл обработки событий.
#![allow(unused)] fn main() { *control_flow = ControlFlow::Wait; }
Установив его в значение ControlFlow::Wait
мы приостанавливаем цикл обработки событий, если события недоступны для обработки. Это идеально подходит для неигровых приложений, которые обновляются только в ответ на ввод пользователя и потребляют значительно меньше энергии/времени процессора, чем ControlFlow::Poll
.
Непосредственная обработка события подразумевает собой разбор перечисления события:
#![allow(unused)] fn main() { match event { Event::WindowEvent { event, .. } => match event { ... }, Event::RedrawEventsCleared => { // render window contents here } _ => {} } }
В случае Event::WindowEvent
мы обрабатываем события оконного менеджера и ввод пользователя.
Когда мы получаем Event::RedrawEventsCleared
, то мы должны отрисовать содержимое окна. Здесь мы будем рендерить нашу графику.
Нам осталось более детально рассмотреть обработку событий окна.
#![allow(unused)] fn main() { Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, WindowEvent::KeyboardInput { input: KeyboardInput { virtual_keycode: Some(VirtualKeyCode::Escape), .. }, .. } => *control_flow = ControlFlow::Exit, WindowEvent::Resized(_) => { // make changes based on window size } _ => {} }, }
WindowEvent::CloseRequested
происходит в случае закрытия окна с помощью x
кнопки окна. Логичной реакцией на это действие - будет завершение работы программы. Чтобы это сделать мы присвоим переменной состояния цикла обработки событий значение ControlFlow::Exit
.
Обработка событий клавиатуры происходит по событию WindowEvent::KeyboardInput
. В этой ситуации если пользователь нажмет клавишу Esc
, мы также завершим работу программы.
Последнее событие котрое мы будем обрабатывать - WindowEvent::Resized
. Оно отвечает за изменение размеров окна. И в этой ситуации я предпологаю что мы должны поменять внутренние переменные нашего приложения для корректного отображения нашей графики.
Остальные события на данном этапе мы не обрабатываем. Чтобы получить более детальную информацию по доступным событиям обратитесь к документации winit
Код этой главы доступен в основном репозитории книги.
Теперь наше окно может быть окончательно закрыто, и мы можем начать рисовать внутри окна.
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. Проведя небольшой рефакторинг, и распределив сервисный код по модулям вашего приложения, он станет более лаконичным.
Код этой главы доступен в основном репозитории книги.
Далее, длинный и "современный" способ нарисовать треугольник на экране.
Компиляция шейдеров
Ранее, мы подключили OpenGL ES контекст к окну, чтобы обрабатывать события от пользователя и выводить графику.
В этой главе, мы будем работать над рендерингом классического треугольника OpenGL. Классический, потому что каждый учебник OpenGL делает это.
Но сначала мы узнаем, как создавать безопасные абстракции в Rust, и создадим инструменты для компиляции шейдера и компоновки программы.
"Современный" OpenGL
Мы будем использовать то, что называется "современным OpenGL". Оказывается, давным-давно это было не так современно. Мы не будем обсуждать здесь графический конвейер с самого начала: вместо этого я предлагаю вам воспользоваться другим "современным учебником OpenGL". Я буду использовать это замечательное руководство в качестве основы. Эта глава будет посвящена реализации на Rust урока "Hello Triangle".
Дополнительная настройка контекста OpenGL
В пердыдущей главе мы говорили о создании окна. В основном мы так и поступали, за исключением того, что я забыл кое-что.
Во-первых, нам нужно указать минимальную версию OpenGL для использования. В нашем случае это можно сделать с помощью атрибутов EGL контекста:
#![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 контекста. Если не настраивать аттрибуты контекста, то графическая подсистема возьмет минимальную версию OpenGL ES. В моем случае это OpenGL ES 1.1.
Чтобы увидеть как это работает, добавьте несколько строк после вызова команды egl.make_current
:
#![allow(unused)] fn main() { println!( "GL_RENDERER = {}", gl::get_string(gl::GL_RENDERER).unwrap_or("Unknown".into()) ); println!( "GL_VERSION = {}", gl::get_string(gl::GL_VERSION).unwrap_or("Unknown".into()) ); println!( "GL_VENDOR = {}", gl::get_string(gl::GL_VENDOR).unwrap_or("Unknown".into()) ); println!( "GL_EXTENSIONS = {}", gl::get_string(gl::GL_EXTENSIONS).unwrap_or("Unknown".into()) ); }
В моем случае это выглядит вот так:
> cargo run
Compiling lesson-02-opengl-context v0.1.0 (/home/opengles-tutorial)
Finished dev [unoptimized + debuginfo] target(s) in 7.27s
Running `/home/opengles-tutorial/target/debug/lesson-02-opengl-context`
GL_RENDERER = GeForce GTX 1050/PCIe/SSE2
GL_VERSION = OpenGL ES 1.1 NVIDIA 460.67
GL_VENDOR = NVIDIA Corporation
GL_EXTENSIONS = GL_EXT_debug_label GL_EXT_map_buffer_range GL_EXT_robustness
GL_EXT_texture_compression_dxt1 GL_EXT_texture_compression_s3tc
GL_EXT_texture_format_BGRA8888 GL_KHR_debug GL_EXT_memory_object
GL_EXT_memory_object_fd GL_NV_memory_object_sparse GL_EXT_semaphore
GL_EXT_semaphore_fd GL_NV_timeline_semaphore GL_NV_memory_attachment
GL_NV_texture_compression_s3tc GL_OES_compressed_ETC1_RGB8_texture
GL_EXT_compressed_ETC1_RGB8_sub_texture GL_OES_compressed_paletted_texture
GL_OES_draw_texture GL_OES_EGL_image GL_OES_EGL_image_external GL_OES_EGL_sync
GL_OES_element_index_uint GL_OES_extended_matrix_palette GL_OES_fbo_render_mipmap
GL_OES_framebuffer_object GL_OES_matrix_get GL_OES_matrix_palette
GL_OES_packed_depth_stencil GL_OES_point_size_array GL_OES_point_sprite
GL_OES_rgb8_rgba8 GL_OES_read_format GL_OES_stencil8 GL_OES_texture_cube_map
GL_OES_texture_npot GL_OES_vertex_half_float
Эта книга нацелена на изучение OpenGL ES версии 2.0 и выше. Поэтому настроим аттрибуты контекста, изменив всего одну строчку:
#![allow(unused)] fn main() { let ctx_attribs = [egl::CONTEXT_CLIENT_VERSION, 2, egl::NONE]; }
Теперь вывод будет совершенно иным:
В моем случае это выглядит вот так:
```txt
> cargo run
Compiling lesson-02-opengl-context v0.1.0 (/home/opengles-tutorial)
Finished dev [unoptimized + debuginfo] target(s) in 7.27s
Running `/home/opengles-tutorial/target/debug/lesson-02-opengl-context`
GL_RENDERER = GeForce GTX 1050/PCIe/SSE2
GL_VERSION = OpenGL ES 3.2 NVIDIA 460.67
...
Помимо того, что теперь мы можем работать с OpenGL ES 3.2, также изменились поддерживаемые OpenGL расширения и их количество. Пока что примите OpenGL расширения как данность, мы обсудим их в следующих главах.
Во-вторых, нам нужно настроить область просмотра. Мы можем сделать это один раз, непосредственно перед первым вызовом gl::clear_color
:
#![allow(unused)] fn main() { gl::viewport(0, 0, 900, 700); }
Шейдеры
Мы создадим вспомогательную функцию для компиляции шейдера из строки, а затем другую функцию для связывания скомпилированных шейдеров с программой.
Сначала попробуйте добавить этот код в перед функцийе main
в файл main.rs
:
#![allow(unused)] fn main() { fn shader_from_source(source: &str) -> u32 { // continue here } }
Учитывая этот код, функция shader_from_source
должна возвращать идентификатор шейдера.
Однако создание шейдера может завершиться неудачно, и мы можем захотеть получить сообщение об ошибке.
Для этого мы изменим тип возвращаемого значения на Result<u32, String>
. Если компиляция шедера завершится удачно, то мы получим идентификатор шейдера, иначе мы сможем извлечь сообщение об ошибке.
Начнем с получения id объекта шейдера:
#![allow(unused)] fn main() { fn shader_from_source(source: &str) -> Result<u32, String> { let id = gl::create_shader(gl::GL_VERTEX_SHADER); // continue here } }
Ха! Нам нужно указать тип шейдера. Давайте улучшим сигнатуру функции и добавим к ней тип шейдера. type
- зарезервированное ключевое слово в Rust, но мы можем назвать его kind
:
#![allow(unused)] fn main() { fn shader_from_source( source: &str, kind: kind: gl::GLenum ) -> Result<Result<u32, String>> { let id = gl::create_shader(kind); // continue here } }
Затем нам нужно установить источник для объекта шейдера, используя обертку к glShaderSource функции. Мы можем установить источник шейдера и скомпилировать его:
#![allow(unused)] fn main() { gl::shader_source(id, source.as_bytes()); gl::compile_shader(id); // continue here }
Мы могли бы вернуть Ok(id)
сейчас и двигаться дальше, однако нам реально нужно видеть правильное сообщение об ошибке, если шейдер не компилируется.
Таким образом, получаем статус компиляции шейдера:
#![allow(unused)] fn main() { let success = gl::get_shaderiv(id, gl::GL_COMPILE_STATUS); }
И если это 0
, мы вернем строку с ошибкой, иначе Ok(id)
:
#![allow(unused)] fn main() { if success == 0 { // continue here } Ok(id) }
Нам нужно будет записать возвращенную ошибку в буфер, поэтому нам нужно знать требуемую длину этого буфера.
Мы получим len
запросив GL_INFO_LOG_LENGTH
для объекта шейдера:
#![allow(unused)] fn main() { let len = gl::get_shaderiv(id, gl::GL_INFO_LOG_LENGTH); // continue here }
При этом мы можем попросить OpenGL записать журнал информации о шейдере в значение нашей ошибки и наконец, мы можем вернуть ошибку :
#![allow(unused)] fn main() { return match gl::get_shader_info_log(id, len) { Some(message) => Err(message), None => Ok(id) }; // continue here }
Финальный код функции shader_from_source
:
#![allow(unused)] fn main() { fn shader_from_source(source: &str, kind: gl::GLenum) -> Result<u32, String> { let id = gl::create_shader(kind); gl::shader_source(id, source.as_bytes()); gl::compile_shader(id); let success = gl::get_shaderiv(id, gl::GL_COMPILE_STATUS); if success == 0 { let len = gl::get_shaderiv(id, gl::GL_INFO_LOG_LENGTH); return match gl::get_shader_info_log(id, len) { Some(message) => Err(message), None => Ok(id) }; } Ok(id) } }
Программа
Программа связывания требует следующих вызовов OpenGL:
#![allow(unused)] fn main() { let program_id = gl::create_program(); gl::attach_shader(program_id, vert_shader); gl::attach_shader(program_id, frag_shader); gl::link_program(program_id); }
По аналогии с компиляцмей шейдеров напишем функцию для программы связывания.
Вместо get_shaderiv
, мы испольуем get_programiv
, instead of get_shader_info_log
we use get_program_info_log
.
#![allow(unused)] fn main() { fn program_from_shaders(vert_shader: u32, frag_shader: u32) -> Result<u32, String> { let program_id = gl::create_program(); gl::attach_shader(program_id, vert_shader); gl::attach_shader(program_id, frag_shader); gl::link_program(program_id); // error handling here let success = gl::get_programiv(program_id, gl::GL_LINK_STATUS); if success == 0 { let len = gl::get_programiv(program_id, gl::GL_INFO_LOG_LENGTH); return match gl::get_program_info_log(program_id, len) { Some(message) => Err(message), None => Ok(program_id) }; } gl::detach_shader(program_id, vert_shader); gl::detach_shader(program_id, frag_shader); Ok(program_id) } }
Одно маленькое уточнение: gl::delete_shader
не удалит шейдер, если он все еще прикреплен к программе. Поэтому мы отключаем шейдер после его связывания.
Использование наших шейдеров и программ связывания
Для начала добавим код шейдеров в файл main.rs
. Я добавил его перед функцией shader_from_source
:
#![allow(unused)] fn main() { pub const VERT_SHADER: &str = r" attribute vec3 Position; void main() { gl_Position = vec4(Position, 1.0); } "; pub const FRAG_SHADER: &str = r" precision mediump float; void main() { gl_FragColor = vec4(1.0, 0.5, 0.2, 1.0); } "; }
Прямо перед основным циклом, мы можем скомпилировать наши вершинные и шейдерные программы:
(main.rs, before loop)
#![allow(unused)] fn main() { let vertex_shader = match shader_from_source(VERT_SHADER, gl::GL_VERTEX_SHADER) { Ok(id) => { println!("Vertex Shader Compiled"); id }, Err(err) => panic!("{}", err) }; let fragment_shader = match shader_from_source(FRAG_SHADER, gl::GL_FRAGMENT_SHADER) { Ok(id) => { println!("Fragment Shader Compiled"); id }, Err(err) => panic!("Error: {}", err) }; // continue here }
Затем мы связываем наши шейдеры:
#![allow(unused)] fn main() { match program_from_shaders(vertex_shader, fragment_shader) { Ok(_) => println!("Program linked"), Err(err) => panic!("Error: {}", err) } // continue here }
Запустим программу на выполнение:
> cargo run
Compiling lesson-02-opengl-context v0.1.0 (/home/opengles-tutorial)
Finished dev [unoptimized + debuginfo] target(s) in 7.27s
Running `/home/opengles-tutorial/target/debug/lesson-02-opengl-context`
GL_RENDERER = GeForce GTX 1050/PCIe/SSE2
GL_VERSION = OpenGL ES 1.1 NVIDIA 460.67
GL_VENDOR = NVIDIA Corporation
GL_EXTENSIONS = GL_EXT_debug_label GL_EXT_map_buffer_range GL_EXT_robustness ...
Vertex Shader Compiled
Fragment Shader Compiled
Program linked
Однако мы пока ничего не увидим на экране, потому что мы еще не отправляем никаких команд рисования в OpenGL. Однако если вывод в терминале должен показать нам что шейдеры скомпилировались и программа слинкована.
Мы исправим это в следующий раз.
Как всегда, код этой части главы на github.
Треугольник
TODO:
Цветной треугольник
TODO:
GL генератор
TODO:
Основные ресурсы
TODO:
Использование библиотеки Failure
TODO:
Формат аттрибутов вершин
TODO:
Процедурные макросы
TODO:
Типы данных вершин
TODO:
Буферы
TODO:
Треугольник и преобразования
TODO:
OpenGL ES 2.0
TODO: