Creating a project
To start this tutorial, we will create a new project from scratch. Even though it's highly recommended to be familiar with Rust and Cargo before starting, some little reminders are always good. Let's start by running:
cargo new --bin my_project
cd my_project
The directory you have just created should contain a Cargo.toml
file which contains our project's metadata, plus a src/main.rs
file which contains the Rust source code. If you have src/lib.rs
file instead, that means that you forgot the --bin
flag ; just rename the file.
In order to use the glium library, we need to add them as dependencies in our Cargo.toml
file:
[dependencies]
glium = "*"
Before we can use them, we also need to import this library in our src/main.rs
file, like this:
#[macro_use]
extern crate glium;
fn main() {
}
It is now time to start filling the main
function!
Создание проекта
Для начала мы создадим новый проект. Хотя и предполагается, что читатель знаком с Rust'ом и Cargo, в руководстве содержится некоторое количество полезных напоминаний. Начнем со следющих команд:
cargo new --bin my_project
cd my_project
Свежесозданная директория содержит файл Cargo.toml
, в котором находятся метаданные проекта, и файл src/main.rs
в котором содержится наш код на Rust. Если вместо последнего у вас src/lib.rs
, то вы забыли параметр --bin и вам нужно просто переименовать файл.
Чтобы воспользоваться библиотекой glium, необходимо добавить ее в наш Cargo.toml
:
[dependencies]
glium="*"
Перед использованием зависимостей, нам также понадобится импортировать их в нашем файле src/main.src
:
#[macro_use]
extern crate glium;
fn main() {
}
Всё готово и мы можем приступать к написанию main
а!
Creating a window
The first step when creating a graphical application is to create a window. If you have ever worked with OpenGL before, you know how hard it is to do this correctly. Both window creation and context creation are platform-specific, and they are sometimes weird and tedious. Fortunately, this is where the glutin library shines.
Initializing a window with glutin can be done by calling glium::glutin::WindowBuilder()::new().build().unwrap()
. However we don't just want to create a glutin window, but a window with an OpenGL context handled by glium. Instead of calling build()
we going to call build_glium()
, which is defined in the glium::DisplayBuild
trait.
fn main() {
use glium::DisplayBuild;
let display = glium::glutin::WindowBuilder::new().build_glium().unwrap();
}
But there is a problem: as soon as the window has been created, our main function exits and display
's destructor closes the window. To prevent this, we need to loop forever until we detect that a Closed
event has been received:
loop {
// listing the events produced by the window and waiting to be received
for ev in display.poll_events() {
match ev {
glium::glutin::Event::Closed => return, // the window has been closed by the user
_ => ()
}
}
}
Right now this code will consume 100% of our CPU, but that will do for now. In a real application you should either use vertical synchronization or sleep for a few milliseconds at the end of the loop, but this is a more advanced topic.
You can now execute cargo run
. After a few minutes during which Cargo downloads and compiles glium and its dependencies, you should see a nice little window.
Создание окна
Первым шагом к написанию графического приложения является создание окна. Если вы когда-либо раньше работали с OpenGL, то наверняка знаете как легко здесь допустить ошибку. Создание окна и контекста платформозависимо и иногда выкидывает неприятные трюки. К счастью, у нас есть библиотека glutin, которая специализируется именно на создании окон.
Инициализировать окно при помощи glutin можно вызвав
glium::glutin::WindowBuilder()::new().build().unwrap()
, но мы не просто хотим создать окно glutin
ом, а хотим получить окно с контекстом OpenGL, с которым будет работать glium.
Поэтому, вместо вызова build()
, мы воспользуемся функцией build_glium()
, объявленной в типаже glium::DisplayBuild
.
fn main() {
use glium::DisplayBuild;
let display = glium::glutin::WindowBuilder::new().build_glium().unwrap();
}
И здесь мы сталкиваемся с проблемой: как только окно создается, наша main
-функция заканчивает выполнение и деструктор закрывает окно. Чтобы это предотвратить, мы воспользуемся бесконечным циклом, проверяющим, не получено ли событие Closed
:
loop {
// listing the events produced by the window and waiting to be received
for ev in display.poll_events() {
match ev {
glium::glutin::Event::Closed => return, // the window has been closed by the user
_ => ()
}
}
}
На данный момент наше приложение будет использовать все ресурсы ЦП, но пока что мы смиримся с этим. В реальном приложении следует либо воспользоваться вертикальной синхронизацией, либо ждать несколько миллисекунд в конце цикла.
Запустите cargo run
и через некоторе время, когда скомпилируются все зависимости и сама программа, вы увидите
созданное нами окно.
Clearing the color
The content of the window, however, is not not very appealing. Depending on your system, it can appear black, show a random image, or just some snow. We are expected to draw on the window, so the system doesn't bother initializing its color to a specific value.
Glium and the OpenGL API work similarly to a drawing software like Window's Paint or The GIMP. We start with an empty image, then draw an object on it, then another object, then another object, etc. until we are satisfied with the result. But contrary to a drawing software, you don't want your users to see the intermediate steps. Only the final result should be shown.
To handle this, OpenGL uses what is called double buffering. Instead of drawing directly to the window, we are drawing to an image stored in memory. Once we have finished drawing, this image is copied to the window.
This is represented in glium by the Frame
object. When you want to start drawing something on your window, you must first call display.draw()
in order to produce a Frame
:
let mut target = display.draw();
We can then use this target
as a drawing surface. One of the operation that OpenGL and glium provide is filling the surface with a given color. This is what we are going to do.
target.clear_color(0.0, 0.0, 1.0, 1.0);
Note that to use this function, we will need to import the Surface
trait first:
use glium::Surface;
The four values that we pass to clear_color
represent the four components of our color: red, green, blue and alpha. Only values between 0.0
and 1.0
are valid. Here we are drawing an opaque blue color.
Like I explained above, the user doesn't immediately see the blue color on the screen. At this point if we were in a real application, we would most likely draw our characters, their weapons, the ground, the sky, etc. But in this tutorial we will just stop here:
target.finish().unwrap();
This call to finish()
means that we have finished drawing. It destroys the Frame
object and copies our background image to the window. Our window is now filled with blue.
Here is our full main
function after this step:
fn main() {
use glium::{DisplayBuild, Surface};
let display = glium::glutin::WindowBuilder::new().build_glium().unwrap();
loop {
let mut target = display.draw();
target.clear_color(0.0, 0.0, 1.0, 1.0);
target.finish().unwrap();
for ev in display.poll_events() {
match ev {
glium::glutin::Event::Closed => return,
_ => ()
}
}
}
}
Заливка цветом
Окно создано, но его содержимое не то что бы очень привлекательно. В зависимости от вашей системы, это может быть черный цвет, случайное изображение или просто шум. Ожидается, что мы будем рисовать в этом окне, поэтому система не беспокоится о смене цвета.
Принципы работы Glium и API OpenGL схожи с оными у графических редакторов вроде Paint'a или GIMP'а. Начиная с чистого холста, мы рисуем на нем объект за объектом, до тех пор, пока не будем удовлетворены результатом. Отличие заключается в том, что мы не хотим демонстрировать пользователю промежуточные шаги. Должен быть показан только итоговый результат.
Чтобы этого добиться, OpenGL использует технику, называемую двойной буферизацией. Она заключается в том, что вместо рисования прямо на экран, программа рисует на изображении, расположенном в памяти, а результат копирует на экран. Этот процесс реализуется в объекте Frame
. Чтобы начать рисовать в окне, нужно вызвать display.draw()
, которая вернет Frame
.
let mut target = display.draw();
Теперь, мы можем использовать target
как поверхность для рисования. Одной из базовых операций OpenGL и, следовательно, Glium, является заполнение поверхности заданным цветом.
target.clear_color(0.0, 0.0, 1.0, 1.0);
Заметьте, что для использования этой функции необходимо сначала импортировать типаж Surface
:
use glium::Surface;
Четыре значения, которые мы передаем в clear_color
, представляют собой компоненты цвета - красный, зеленый, синий и альфа-канал. Значения должны находиться в диапазоне от 0.0 до 1.0. В данном случае мы рисуем бледно-синий цвет.
Как объяснялось выше, пользователь не видит синий цвет сразу же. На этом этапе в реальных приложениях обычно отрисовываются персонажи, их экипировка, мир, небо и прочее, но в данном руководстве мы пока остановимся на следующем коде:
target.finish().unwrap();
Вызов finish()
означает, что мы закончили с рисованием. Он уничтожает объект Frame
и копирует изображение из памяти на экран. Наше окно теперь залито синим цветом.
На данном этапе наша функция main
выглядит так:
fn main() {
use glium::{DisplayBuild, Surface};
let display = glium::glutin::WindowBuilder::new().build_glium().unwrap();
loop {
let mut target = display.draw();
target.clear_color(0.0, 0.0, 1.0, 1.0);
target.finish().unwrap();
for ev in display.poll_events() {
match ev {
glium::glutin::Event::Closed => return,
_ => ()
}
}
}
}