steps3D - Tutorials - Работа с библиотекой GLFW

Работа с библиотекой GLFW

Традиционно для создания простых примеров, использующих OpenGL, используется библиотека GLUT. Однако она давно уже не поддерживается и не является open-source. У нее есть открытый аналог - библиотека freeglut (на сайте есть статья по этой библиотеке), которая добавляет поддержку ряда новых возможностей.

Однако в последнее время довольно часто в различных примерах стала использоваться библиотека GLFW. Она обладает открытым исходным текстом, поддерживает все основные операционные системы и проста в использовании. Кроме того, она предоставляет новые возможности, которых нет в исходной библиотеке GLUT. Здесь мы рассмотрим основы работы с этой библиотекой приведем простые примеры использование ее.

Одной интересной возможностью данной библиотеки является заявленная для версии 3.2 поддержка Vulkan.

Начало работы

Для начала нужно скачать саму эту библиотеку (www.glfw.org), мы здесь будем рассматривать версию 3.1. Обычно скачивается исходный код и далее выполняется сборка библиотеки под конкретную платформу. Вместо традиционных проектов для сборки библиотеки GLFW используется утилита CMake (если у вас ее нет, то ее можно скачать с сайта www.cmake.org). Этап утилита на основании описаний проекта для сборки создает все необходимые файлы проекта для самых разных платформ, начиная с обычных Makefile-ов и заканчивая проектами под различные версии популярных IDE.

После того, как вы скачали и распаковали все исходники библиотеки, перейдите в ее каталог и запустите команду cmake, передав при помощи ключа -G для какой именно платформы необходимо создать проект для сборки.

Так например 'cmake -G "Visual Studio 14 2015"' создаст проект для Microsoft Visual Studio 2015, который может быть использовн для сборки примера. Аналогично 'cmake -G "NMake Makefiles"' создаст Makefile который может быть использовн для сборки проекта при помощи утилиты nmake. После создания проекта выполните сборку самой библиотеки при помощи созданного проекта.

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

Рассмотрим теперь создание простейшего приложения на OpenGL с использованием библиотеки GLFW. Первым шагом будет включение соответствующего заголовочного файла:

#include <GLFW/glfw3.h>

Обратите внимание, что этот файл сам включает заголовочный файл для OpenGL, а также определяет необходимые определения из файла windows.h не включая самого этого файла. Если вам все-таки нужно включать файл windows.h то сделаете это перед включением glfw3.h.

Если вам нужен файл glu.h, то вместо его явного включения можно определить макро GLFW_INCLUDE_GLU перед загрузкой файла glfw3.h как показано ниже.

#define  GLFW_INCLUDE_GLU
#include <GLFW/glfw3.h>

Для начала работы с библиотекой GLFW ее нужно проинициализировать при помощи вызова glfwInit. В случае ошибки эта функция возвращает значение GL_FALSE.

if (!glfwInit())
    exit(-1);

Когда вы завершаете работу с GLFW, то следует вызывать функцию glfwTerminate. Этот вызов закроет все открытые окна и освободит выделенные ресурсы.

Библиотека GLFW позволяет устанавливать различные callback-функции. Одной из таких функций является функция, вызываемая при возникновении каких-либо ошибок. Она получает на вход целочисленный код ошибки и строку с описанием ошибки. Ниже приводится пример такой callback-функции просто печатающей сообщение об ошибке.

void error(int code, const char * desc)
{
    fputs(desc, stderr);
}

Для установки этого обработчика служит функция glfwSetErrorCallback:

glfwSetErrorCallback(error);

Создание окон и контекстов

Для создания окна и контекста OpenGL в GLFW служит функция glfwCreateWindow. Само окно при этом идентифицруется при помощи указателя на структуру GLFWwindow. Ниже приводится пример создания окна с использованием этой функции.

GLFWwindow * window = glfwCreateWindow ( 640, 480, "My window", NULL, NULL);

if (window == NULL)
{
    glfwTerminate();
    exit (-1);
}

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

Следующий пример демонстрирует создание окна на первом мониторе с заданным разрешением.

GLFWwindow * window = glfwCreateWindow ( 640, 480, "", glfwGetPrimaryMonitor(), NULL);

Ниже приводится пример кода, получающего свойства первичного монитора и создающего полноэкранное окно на нем.

const GLFWmonitor * monitor = glfwGetPrimaryMonitor();
const GLFWvidmode * mode    = glfwgetVideoMode ( monitor );

glfwWindowHint ( GLFW_RED_BITS, mode->redBits );
glfwWindowHint ( GLFW_GREEN_BITS, mode->greenBits );
glfwWindowHint ( GLFW_BLUE_BITS, mode->blueBits );
glfwWindowHint ( GLFW_REFRESH_RATE, mode->refreshRate );

GLFWwindow * window = glfwCreateWindow ( mode->width, mode->height, "My window", monitor, NULL );

Для уничтожения окна служит функция glfwDestroyWindow.

void glfwDestroyWindow (GLFWwindow * window);

Прежде чем мы сможем использовать команды для рендеринга в созданное окно его нужно сделать его текущим контекстом при помощи функции glfwMakeContextCurrent:

void glfwMakeContextCurrent (GLFWwindow * window);

Если пользователь во время работы приложения попробует закрыть окно, то оно на самом деле не будет закрыто, просто у него будет выставлен флаг, что данное окно должно быть закрыто. Проверить, выставлен ли этот флаг у данного окна, можно при помощи функции glfwWindowShouldClose.

int glfwWindowShouldClose (GLFWwindow * window);

Этот флаг также можно явно выставить из кода при помощи функции glfwSetWindowShouldClose:

void glfwSetWindowShouldClose (GLFWwindow * window, int value);

Рендеринг при помощи OpenGL

Как только был задан текущий контекст, то сразу же после этого можно начать рендеринг, используя для этого команды OpenGL. Текущий размер окна всегда можно получить при помощи glfwGetFramebufferSize:

void glfwGetFramebufferSize (GLFWwindow * window, int * width, int * height);

Чтобы не опрашивать каждый раз размер окна можно (как и в GLUT) установить обработчик (callback<-функцию) на событие изменения размера заданного окна, как это показано ниже:

void resize (GLFWwindow * window, int w, int h)
{
    . . .
}

. . .

glfwSetFramebufferSizeCallback (window, resize);

По умолчанию GLFW использует двойную буферизацию, поэтому после того, как рендеринг текущего кадра завершен, нужно просто вызвать функцию glfwSwapBuffers.

void glfwSwapBuffers (GLFWwindow * window);

Стандартное поведение в ответ на этот вызов заключается в немедленном переключении буферов. Однако можно явно задать сколько кадров нужно ждать перед тем, как выполнить переключение буферов. Этот параметр обычно называется vsync и рекомендуется использовать для него значение, равное единице. Для задания этого параметре служит функция glfwSwapInterval.

Очередь событий, главный цикл

Как и в GLUT, библиотека GLFW имеет цикл обработки событий. Только здесь этот цикл явный, а не спрятан как в GLUT (в функции glutMainLoop).

В GLFW есть всего несколько функций, которые работают с сообщениями. Первая из них - glfwPollEvents - берет сообщения из очереди и обрабатывает их. Если в очереди нет ни одного сообщения, то управление немедленно возвращается. Это очень удобно для организации анимаций, игр и т.п. При этом основной цикл обработки сообщений обычно выглядит следующим образом:

while (!glfwWindowShouldClose(window))
{
    display         ();
    glfwSwapBuffers ();
    glfwPollEvents  ();
}

Если нет необходимости в постоянной перерисовке содержимого окна, то для обработки сообщений можно использовать функцию glfwWaitEvents. Ее отличием от glfwPollEvents является то, что если сообщений в очереди нет, то нить "засыпает" до их появления.

С другой стороны, когда возникает необходимость "разбудить" заснувшую" нить, можно использоувать функцию glfwPostEmptyEvent.

Обработка событий

Подобно библиотеке GLUT можно задать функции-обработчики для ряда событий, таких как изменение размера окна, ввод с клавиатуры, события от мыши и т.п. Весь ввод с клавиатуры GLFW делит на две категории - key events и character events. Первые связаны с физическими клавишами клавиатуры (нажатие и отпускание клавиш), а вторые - с генерацией символов клавиатурой (подобно тому как это сделано в Windows).

Функция- обработчик нажатий и отпусканий клавишь имеет следующий вид:

void keyCallback (GLFWwindow * window, int key, int scancode, int action, int mode)
{
    . . .
}

Параметр key задает клавишу на клавиатуре, например GLFW_KEY_SPACE или GLFW_KEY_ESCAPE. Если соответствующая клавиша неизвестна GLFW, то этот параметр будет равен GLFW_KEY_UNKNOWN.

Параметр action определяет произошедшее с клавишей событие и равен GLFW_PRESS, GLFW_RELEASE или GLFW_REPEAT.

Параметр scancode идентифицирующее клавишу на клавиатуре, его значение зависит от платформы.

Параметр mode - это набор битовых флагов, задающих состояние клавиш-модификторов. Этими клавишами являются GLFW_MOD_SHIFT, GLFW_MOD_CONTROL, GLFW_MOD_ALT и GLFW_MOD_SUPER.

Также можно узнать состояние любой клавиши при помощи функции glfwGetKey, но при этом следует иметь в виду, что возвращается не текущее состояние клавиши, а кэшированное значение. Т.е. если нажатая клавиша была отпущена до того, как вы опросили ее состояние, то вы потеряете ее нажатие. Для борьбы с этим служит следующий вызов:

glfwSetInputMode (window, GLFW_STICKY_KEYS, 1);

Функция glfwGetKey возвращает одно из следующих двух значнений - GLFW_PRESS и GLFW_RELEASE.

int state = glfwGetKey ( window, GLFW_KEY_A );

if ( state == GLFW_PRESS )      // handle press of A key
   . . .

Обработчик key events задается при помощи функции glfwSetKeyCallback:

glfwSetKeyCallback ( window, keyCallback );

Для получения символов в UNICODE, возникающих при нажатии или отпускании клавишь, служит функция-обработчик следующего вида:

void charCallback ( GLFWWindow * window, unsigned int ch )
{
    // handle char
}

Подобная функция-обработчик задается при помощи команды glfwSetCharCallback:

glfwSetCharCallback ( window, charCallback );

Легко можно установить обработчик перемещения курсора мыши при помощи функции glfwSetCursorPosCallback. Сама функция-обработчик имеет следующий вид:

void mouseCallback ( GLFWwindow * window, double x, double y )
{
    // handle mouse movement
}

Также можно в произвольный момент времени поолучить координаты курсора мыши при помощи вызова функции glfwGetCursorPos как это показано ниже.

double x, y;

glfwGetCursorPos ( window, &x, &y );

Можно установить обработчик, который будет вызываться когда курсор мыши входит в или покидает клиентскую часть окна, при помощи функции glfwSetCursorEnterCallback. Сама функция-обработчик имеет при этом следующий вид:

void enter ( GLFWwindow * window, int intered )
{
    // handle cursor enter or leave
}

Еще одним обработчиком событий для мыши является обработка нажатий и отпусканий клавишь мыши. Этот обработчик устанавливается при помощи функции glfwSetMouseButtonCallback и имеет следующий вид:

void mouseKey ( GLFWWindow * window, int button, int action, int mode )
{
    if ( button == GLFW_MOUSE_BUTTON_RTIGHT && action == GLFW_PRESS )
        // handle right mouse-click
}

Также можно установить и обработчик scroll-событий. Для этого служит функция glfwSetScrollCallback и обработчик имеет следующий вид:

void scroll ( GLFWWindow * window, double xOffs, double yOffs )
{
    . . .
}

Прочий ввод

В GLFW легко можно получить время в секундах, прошедшее с момента запуска программы, при помощи функции glfwGetTime:

double glfwGetTime ();

Более того, GLFW поддерживает работу с буфером обмена, правда он моежт получать из него только строки в кодировке UTF-8. Для этого служит функции glfwGetClipboardString и glfwSetClipboardString:

const char * glfwGetClipboardString( GLFWwindow * win );
void         glfwSetClipboardString ( GLFWwindow * win, const char * string );

Также есть поддержка drag'n'drop - при помощи функции glfwSetDropCallback можно задать функцию-обработчик, которая будет вызываться при "бросании" файла в окно, на вход функцию получит пути на "брошенные" файлы:

void drop ( GLFWwindow * win, int count, const char ** paths )
{
    for ( int i = 0; i < count; i++ )
        printf ( "%s\n", paths [i] );
}

Работа с курсором мыши

В отличии от GLUT у библиотеки GLFW есть хорошая поддержка создания и работы с курсором мыши. Довольно часто мышь используется для вращения или перемещения объекта. В это время желательно, чтобы курсор мыши стал невидимым и текущее окно все равно получало бы все сообщения от мыши, даже если курсор вышел за пределы текущего окна. Для включения этого режима служит следующий вызов:

glfwSetInputMode ( win, GLFW_CURSOR, GLFW_CURSOR_HIDDEN );

Для возвращения кусроа в нормальный ержи служит следующий вызов:

glfwSetInputMode ( win, GLFW_CURSOR, GLFW_CURSOR_NORMAL );

Кроме того, GLFW поддерживает ряд стандартных курсоров. Их можно получить при помощи функции glfwCreateStandardCursor и сделать текущим курсором для заданного окна при помощи glfwSetCursor:

GLFWCursor * cursor = glfwCreateStandardCursor ( GLFW_HRESIZE_CURSOR );
glfwSetCursor ( win, cursor );

Стандартными курсорами для GLFW являются GLFW_ARROW_CURSOR, GLFW_IBEAM_CURSOR, GLFW_CROSSHAIR_CURSOR, GLFW_HAND_CURSOR, GLFW_HRESIZE_CURSOR и GLFW_VRESIZE_CURSOR.

Можно создать свой собственнй курсор по заданному RGBA-изображению. Ниже приводится пример соответствующего кода.

unsigned char pixels [16*16*4];
int           xHotspot = 0, yHotspot = 0;

memset ( pixels, 0xFF, sizeof(pixels) );

GLFWimage image;

image.width   = 16;
image.height  = 16;
image.pixels  = pixels;

GLFWcursor * cursor = glfwCreateCursor ( &image, xHotspot, yHotspot );

Для уничтожения курсора служит следующая функция:

glfwDestroyCursor ( cursor );

Дополнительные возможности

С каждым создаваемым окном можно связать некоторый, задаваемый пользователем, указатель. Для этого служит функция glfwSetWindowUserPointer. Получить значение этого указателя можно при помощи функции glfwGetWindowUserPointer:

void   glfwSetWindowUserPointer ( GLFWwindow * window, void * pointer );
void * glfwGetWindowUserPointer ( GLFWwindow * window );

Легко можно получить доступные мониторы для создания окон при помощи следующего кода:

int                 count;
const GLFWmonitor * monitors = glfwGetMonitors ( &count );

Еще одной доступной информацией является список поддерживаемых видеорежимов, который можно получить при помощи приведенного ниже кода:

int         count;
const GLFWvidmode * modes = glfwGetVideoModes ( &count );

Перед созданием окна можно задать желаемые свойства для окна и создаваемого с ним контекста - так называемые window hints. Для этого служит функция glfwWindowHint:

void glfwWindowHint ( int target, int hint );

Некоторые из этих пожеланий являются "жесткими" (hard). Это означает, что они должны быть выполнены точно. Если это не получается, то возникает ошибка. К числу "жестких пожеланий" относятся следующие - GLFW_STEREO, GLFW_DOUBLEBUFFER, GLFW_CLIENT_API, GLFW_OPENGL_FORWARD_COMPAT и GLFW_OPENGL_PROFILE.

Остальные "пожелания" будут учтены про мере возможности, но точного совпадения может и не быть. Рассмотрим теперь какие именно "пожелания" поддерживает GLFW.

Есть ряд "пожеланий", непосредственно связанных с самим окном. К ним относятся следующие пожелания:

Ряд "пожеланий" свзяаны с фреймбуфером. К ним относятся:

Еще одна группа пожеланий относится к создаваемому контексту.

Ниже приводится простейший пример использования GLFW - используется OpenGL 1 и не используется никаких шейдеров.

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <stdlib.h>
#include <stdio.h>

void error ( int error, const char * description )
{
    fputs ( description, stderr );
}

void key ( GLFWwindow * window, int key, int scancode, int action, int mods )
{
    if ( key == GLFW_KEY_ESCAPE && action == GLFW_PRESS )
        glfwSetWindowShouldClose ( window, GL_TRUE );
}

void display ( GLFWwindow * window )
{
    int width, height;
    
    glfwGetFramebufferSize ( window, &width, &height );

    float   ratio = width / (float) height;
    
    glViewport(0, 0, width, height);
    glClear(GL_COLOR_BUFFER_BIT);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(-ratio, ratio, -1.f, 1.f, 1.f, -1.f);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glRotatef((float) glfwGetTime() * 50.f, 0.f, 0.f, 1.f);
    glBegin(GL_TRIANGLES);
    glColor3f(1.f, 0.f, 0.f);
    glVertex3f(-0.6f, -0.4f, 0.f);
    glColor3f(0.f, 1.f, 0.f);
    glVertex3f(0.6f, -0.4f, 0.f);
    glColor3f(0.f, 0.f, 1.f);
    glVertex3f(0.f, 0.6f, 0.f);
    glEnd();
}

int main ()
{
    glfwSetErrorCallback ( error );
    
    if ( !glfwInit() )
        exit ( 1 );
    
    GLFWwindow * window = glfwCreateWindow ( 640, 480, "Simple example", NULL, NULL );
    
    if ( !window )
    {
        glfwTerminate ();
        exit ( 1 );
    }
    
    glfwMakeContextCurrent ( window );
    glfwSwapInterval       ( 1 );
    glfwSetKeyCallback     ( window, key);
    
    while ( !glfwWindowShouldClose ( window ) )
    {
        display ( window );
        glfwSwapBuffers ( window );
        glfwPollEvents  ();
    }
    
    glfwDestroyWindow ( window );
    glfwTerminate     ();
    
    return 0;
}

Следующий пример основан на OpenGL 4.4 и шейдерах, также использует библиотеку GLEW и ряд вспомогательных классов.

#include <GL/glew.h>
#include    <GLFW/glfw3.h>
#include    <stdlib.h>
#include    <stdio.h>
#include    "Program.h"
#include    "VertexArray.h"
#include    "VertexBuffer.h"
#include    "glUtilities.h"
#include    "mat4.h"

#define NUM_VERTICES    3
#define VERTEX_SIZE     (3*sizeof(float))

static const float vertices [] = 
{
    -1.0f, -1.0f, 0.0f,
     0.0f,  1.0f, 0.0f,
     1.0f, -1.0f, 0.0f
};

Program         program;
VertexArray     vao;
VertexBuffer    buf;

void error ( int error, const char * description )
{
    fputs ( description, stderr );
}
void key ( GLFWwindow * window, int key, int scancode, int action, int mods )
{
    if ( key == GLFW_KEY_ESCAPE && action == GLFW_PRESS )
        glfwSetWindowShouldClose ( window, GL_TRUE );
}

void display ( GLFWwindow * window )
{
    glClear ( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );

    vao.bind ();    
    glDrawArrays ( GL_TRIANGLES, 0, NUM_VERTICES );     
    vao.unbind ();
}

void    reshape ( GLFWwindow * window, int w, int h )
{
    glViewport ( 0, 0, (GLsizei)w, (GLsizei)h );
}

int main ()
{
    glfwSetErrorCallback ( error );
    
    if ( !glfwInit() )
        exit ( 1 );
    
    glfwWindowHint ( GLFW_RESIZABLE, 1 );
    glfwWindowHint ( GLFW_DOUBLEBUFFER, 1 );
    glfwWindowHint ( GLFW_DEPTH_BITS, 24 );
    glfwWindowHint ( GLFW_CLIENT_API, GLFW_OPENGL_API );
    glfwWindowHint ( GLFW_CONTEXT_VERSION_MAJOR, 4 );
    glfwWindowHint ( GLFW_CONTEXT_VERSION_MINOR, 4 );
    glfwWindowHint ( GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE );
    glfwWindowHint ( GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE );

    GLFWwindow * window = glfwCreateWindow ( 640, 480, "Simple example", NULL, NULL );
    
    if ( !window )
    {
        glfwTerminate ();
        exit ( 1 );
    }

    glfwMakeContextCurrent ( window );
    
    glewExperimental = GL_TRUE;
    glewInit   ();
    glGetError ();
    
    if ( !program.loadProgram ( "simple.glsl" ) )
    {
        printf ( "Error building program: %s\n", program.getLog ().c_str () );
        
        exit ( 2 );
    }
    
    program.bind ();
    vao.create   ();
    vao.bind     ();
    buf.create   ();
    buf.bind     ( GL_ARRAY_BUFFER );
    buf.setData  ( NUM_VERTICES * VERTEX_SIZE, vertices, GL_STATIC_DRAW );

    program.setAttrPtr ( "position", 3, VERTEX_SIZE, (void *) 0 );

    buf.unbind     ();
    vao.unbind     ();
    
    glfwMakeContextCurrent    ( window );
    glfwSwapInterval          ( 1 );
    glfwSetKeyCallback        ( window, key );
    glfwSetWindowSizeCallback ( window, reshape );
    
    while ( !glfwWindowShouldClose ( window ) )
    {
        display         ( window );
        glfwSwapBuffers ( window );
        glfwPollEvents  ();
    }
    
    glfwDestroyWindow ( window );
    glfwTerminate     ();
    
    return 0;
}

По этой ссылке можно скачать весь исходный код к этой статье.