[lang_en]This article is avaliable only in russian.[/lang_en]

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

OpenGL — открытая графическая библиотека для работы с двухмерной и трехмерной графикой. Главной особенностью можно считать то, что она поддерживается практически всеми операцинными системами. Тем самым давая возможность обладателям Windows, Linux, MacOs и др. операционных систем творить свои трехмерные шедевры, в отличие от DirectX, который написан только под Windows. Однако в каждой операцинной системе инициализация OpenGL проводится по своему. Я мог бы конечно рассказать вам об этом, но главная цель моей книги — это показать новые возможности OpenGL с использованием современных видеокарт, поэтому для инициализации мы будем использовать библиотеку SDL. Это кросс платформенный мультимедийный API или иными словами, с помощью этой библиотеки мы сможем, будь-то Windows, Linux или MacOS, работать с графикой, клавиатурой и мышкой, таймером, событиями и т.д. Так что изучение OpenGL  не будет зависеть от выбора операционной системы.

Для начала ознакомимся с этой библиотекой и её возможностями. Я расскажу только о том, что нам потребуется в дальнейшей работе.

?нициализация SDL

Прежде всего нужно подключить файл прототипов SDL функций

#include «SDL.h»

В него входят все разделы этой библиотеки, поэтому для написания очередной программы нужно включать этот файл, независимо от того, нужен звук или видео.

Перед тем как использовать эту библиотеку, нам потребуется её инициализировать, для этого существует специальная функция:

SDL_Init(<флаги>), где флаги должны указывать на то, какие части этой библиотеки для нас нужны среди следующих

SDL_INIT_VIDEO                            | видео

SDL_INIT_AUDIO                           | аудио

SDL_INIT_CDROM                         | cd привод

SDL_INIT_TIMER                           | таймер

SDL_INIT_JOYSTICK                    | джойстик

SDL_INIT_EVERYTHING             | все части

Таблица 1.1

Т.е. если нам потребуется использовать в своём приложении видео и звук, инициализация будет выглядеть так

SDL_Init( SDL_INIT_VIDEO | SDL_INIT_AUDIO);

При чем все свои функции SDL будет загружать с динамической библиотеки (в Windows — это SDL.dll, в Linux – это SDL.so и т.д), которая должна быть расположена в директории нашего приложения, иначе программа её не найдёт. Однако существует возможность установить нужную нам альтернативную директории, в которой будет осуществляться поиск. Это можно сделать с помощью

SDL_SetLibraryPath(<путь к нашей дериктории>)

Но запомните, что это нужно указывать до вызова функции инициализации.

Есть еще одна функция для инициализации

SDL_InitSubSystem(<флаги>). Она имеет то же значение, что и SDL_Init, однако даёт возможность инициализировать каждую подсистему в отдельности. Например, вы сначала загрузите видео и проведёте какие-то вычисления, а потом загрузите аудио и т.д.

Когда же мы завершили нашу работу, то перед выходом из программы нам нужно выгрузить библиотеку SDL из памяти (не будем писать ни каких вирусов, освободим память)

SDL_Quit();

Для завершения работы не всего приложения, а какой-либо её части можно использовать

SDL_QuitSubSystem(<флаги>)

Вот и весь процесс инициализации и выгрузки библиотеки.

Ещё один совет – не забывайте проверять на возврат положительного результата. Ведь для инициализации мы используем функцию, которая возвращает определенное значение. Причем, если это значение ракно нулю, то тогда библиотека не загрузилась и мы просто обязаны выйти из программы (при этом выдав сообщение с ругательствами по поводу ошибки или записан информацию об ошибке в отдельный файл, так называем LOG-файл), иначе продолжение работы приведёт к ошибке. Вот вам подобная ситуация. Сделали вы классную игру, а тестер взял да и убрал динамическую библиотеку, тогда программа запустится и начнутся ошибки, а из-за чего не понятно. А если добавить сообщение с выходом в подобной ситуации, то программа выдаст этому тестеру сообщение суровое и закроется. Сразу всё станет понятно. Лучше всегда делать подобные вещи, тогда легче потом будет отлаживать своё творение (находить в нем ошибки). Ведь проверку на возвращение положительного результат можно вставлять во многие функции, так лучше не упускать этой возможности. В листинге 1.1 привожу код нашей маленькой программки, она находится в ex01. Здесь я использую ещё одну новую функцию

SDL_GetError(), которая возвращает строку с информацией об ошибке.

После запуска этой программы вы ничего не увидите, ни какого окна и т.д., однако в директории этой программы должен появиться файл “stdout.txt”, где будет написан текст, который выводили с помощью printf. Это и есть наш LOG-файл, его очень удобно использовать в целях отладки. В случае неудачной инициализации SDL появиться файл stderr, который будет содуржать информацию об ошибке. Для этого мы использовали fprintf.

#include <stdlib.h>

#include “SDL.h”

int main(int argc, char *argv[])

{

//— инициализация SDL

if ( SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) < 0 ) {

fprintf(stderr, “Не могу инициализировать SDL: %s\n”, SDL_GetError() );

exit(-1);

}

printf(“?нициализация SDL завершена успешно”);

//— освобождаем библиотеку

SDL_Quit();

Exit(0);

}

листинг 1.1

Проверить на удачную инициализацию можно ещё и с помощью функции

SDL_WasInit(<флаги>), где флаги могут принимать значение из таблицы 1.1, а возвращаемое значение будет состоят из флагов тех частей, которые успешно инициализировались. Т.е., например, нам нужно проверить на успешность видео и аудио, тогда указываем в качестве аргумента SDL_INIT_EVERYTHING и проверяем возвращаемое значение на наличие нужных в нем нам флагов:

Uint32 subsystem_init=SDL_WasInit(SDL_INIT_EVERYTHING);

if(subsystem_init&SDL_INIT_VIDEO)

printf(«Video is initialized.\n»);

else printf(«Video is not initialized.\n»);

Однако программа наша пока не очень и впечатляет – нет никакого видимого результата. Ну ни чего, дальше результаты будут все более зрелищными.

В?ДЕО

Создание окна и обработка событий

? так давайте освоим одну из главных частей SDL – видео (при инициализации указываем флаг SDL_INIT_VIDEO). Эта часть библиотеки даёт нам возможность создавать окно (та область, где будет происходить все действия), менять разрешение экрана (какая же игра без этого), загружать изображения (хотя мы будем использовать другую, более функциональную библиотеку для этого), выводить их на экран, а также рисовать, используя прямое обращение к видео карте. ? конечно же, самое главное – рисовать в этом окне с помощью OpenGL. Теперь все это, но по-подробней.

В основе работы видео лежит графическая поверхность и тип данных её задающий – SDL_Surface. Область графических данных обладает своей политрой и своим форматом пикселей.  Поверхностью может быть как видимой, так и невидимой. Видимая поверхность – это канва (область рисования) нашего окна. Невидимую мы будем использовать для загрузки  изображения и хранения изображений и т.д. Вот основное понятие, которое нужно знать.

Создать окно можно с помощью функции

SDL_SetVideoMode(<длина>,<высота>,<глубина цвета>,<флаги>), где

Длина и высота – это самые обыкновенные размеры нашего окна

Глубина цвета может принимать значение нуля, тогда для окна будет использоваться значение, используемое на экране.

Флаги задают свойства нашего окна, а также определяют цели его использования (если мы создаём приложение с поддержкой OpenGL, то указать должны это во флагах и т.д.)

SDL_SWSURFACE Создаёт видео поверхность в системной памяти
SDL_HWSURFACE Создаёт видео поверхность в видео памяти
SDL_ASYNCBLIT Включает синхронизацию обновлений с поверхностью. Это понизит скорость вывода на однопроцессорных компьютерах, но повысит на многопроцессорных станциях
SDL_ANYFORMAT Если указанная глубина цвета не поддерживается, она будет эммулироваться SDL
SDL_HWPALETTE Даёт SDL эксклюзивный доступ к палитре.
SDL_DOUBLEBUF Включение режима двойной буферизации. Возможно только вместе с флагом SDL_HWSURFACE
SDL_FULLSCREEN Полноэкранный режим
SDL_OPENGL Создаёт контекст воспроизведения OpenGL. Перед этим нужно выставить параметры работы с помощью SDL_GL_SetAttribute
SDL_OPENGLBLIT Создаёт контекст воспроизведения OpenGL, но с возможность использования обычных операций вывода. 2D поверхность экрана будет иметь альфа-поверхность.  ?спользуйте SDL_UpdateRects для обновления изменений на поверхности экрана.
SDL_RESIZABLE Создание окна с возможностью изменения своих размеров пользователем. При этом генерируется новое сообщение SDL_VIDEORESIZE при таких изменениях.
SDL_NOFRAME Создание окна без заголовка и оконтовки. В полноэкранном режиме этот флаг выставляется автоматически

Таблица 1.1

Функция возвращает поверхность окна в случае успешного завершения операции или возвращает NULL в случае провала.

Вот простой пример на использование этой функции. Допишите код листинга 1.2 в предыдущую программу после инициализации SDL. Эта программа находится в ex.02.

{ SDL_Surface *screen;

screen = SDL_SetVideoMode(640, 480, 16, SDL_SWSURFACE);

if ( screen == NULL ) {// Если установить разрешение не удалось

fprintf(stderr, «Невозможно установить разрешение 640×480: %s\n», SDL_GetError());

exit(-1);

}

}

листинг 1.2

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

typedef union{

Uint8 type; // тип нашего события

//— Параметры

SDL_ActiveEvent active;

SDL_KeyboardEvent key;

SDL_MouseMotionEvent motion;

SDL_MouseButtonEvent button;

SDL_JoyAxisEvent jaxis;

SDL_JoyBallEvent jball;

SDL_JoyHatEvent jhat;

SDL_JoyButtonEvent jbutton;

SDL_ResizeEvent resize;

SDL_ExposeEvent expose;

SDL_QuitEvent quit;

SDL_UserEvent user;

SDL_SywWMEvent syswm;

} SDL_Event;

Как можно увидеть из описания  – событие состоит из переменной, описывающей тип события, и параметров, поступающих к этому событию. Причем параметров довольно много, но это просто весь их перечень, использоваться из них будет только один – соотвествующий поступившему типу. Например, если нам пришло событие щелчка кнопкой мыши, то из всех параметров мы будем использовать SDL_MouseButtonEvent button, где хранится состояние мыши (какой кнопкой был произведен щелчок и т.д.). В таблице 1.3 представлен списов типов события и соответствующие параметры для них:

SDL_ACTIVEEVENT SDL_ActiveEvent
SDL_KEYDOWN/UP SDL_KeyboardEvent
SDL_MOUSEMOTION SDL_MouseMotionEvent
SDL_MOUSEBUTTONDOWN/UP SDL_MouseButtonEvent
SDL_JOYAXISMOTION SDL_JoyAxisEvent
SDL_JOYBALLMOTION SDL_JoyBallEvent
SDL_JOYHATMOTION SDL_JoyHatEvent
SDL_JOYBUTTONDOWN/UP SDL_JoyButtonEvent
SDL_QUIT SDL_QuitEvent
SDL_SYSWMEVENT SDL_SysWMEvent
SDL_VIDEORESIZE SDL_ResizeEvent
SDL_VIDEOEXPOSE SDL_ExposeEvent
SDL_USEREVENT SDL_UserEvent

Таблица 1.3

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

1)       SDL_PollEvent (<указатель на SDL_Event структуру>), которая возвращает 0 если очередь событий пуста и возвращает 1 если кроме полученного события при вызове этой функции остались ещё другие в очереди. Т.е. чтобы нам ею воспользовать, введём переменную события

SDL_Event         event;

Затем будем в цикле вызывать функцию SDL_PollEvent до тех пор, пока очередь не закончиться (пока SDL_PollEvent не возвратит значение нуля)

While ( SDL_PollEvent( &event ) ) {

Теперь мы можим реагировать на события в зависимости от их типа

Switch ( event.type )

{

Если, например, нам попадётся сообщение о выходе

Case SDL_QUIT

//— пора закрывать нашу программу

Break;

Вот таким иобразом организуется получение сообщений

2)       SDL_PushEvent( <указатель на SDL_Event структуру> ), которая возвращает 0 при удачном завершении операции и –1 при провале. В общем мы можем использовать эту функцию для добавления нового, указанного нами события в очередь.

Как вы наверное уже заметили в таблице 1.3 среди перечисленных типов события есть SDL_USEREVENT, т.е. событие, которое не использует система. Оно предназначено для пользователя, для того чтобы мы могли создавать своё событие и помещать туда нужные нам данные, а потом обрабатывать его со всеми. Привожу пример, как это делается:

SDL_Event user_event; // наше событие

user_event.type=SDL_USEREVENT; //его тип – пользовательский

//— теперь заносим нужные нам данные

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

user_event.user.code=2; // 1- переменная, хранящая целые числа (код какой-нибудь)

user_event.user.data1=NULL;  // 2 – переменная, хранящая указатель ( VOID* )

user_event.user.data2=NULL;  // 3 – переменная, хранящая указатель ( VOID* )

//— добавляем событие в очередь ко всем остальным

SDL_PushEvent(&user_event);

Вот и все пока, что касается событий, а типы параметров мы рассмотрим чуть поздже. Теперь давайте на основе этих знаний построим приложение, выводящее простое окно, но уже с использованием обработки событий. (Обрабатываем события до получения SDL_QUIT, а затем выходим из программы) В ex.03 представлен код такой программы.

bool run = true;

SDL_Event event;

while (run)

{

while ( SDL_PollEvent( &event ) )

{

switch ( event.type )

case SDL_QUIT:

run = false;

break;

}

}

листинг 1.2

В листинге 1.2 представлен код зацикливания программы до того момента, пока мы не получим нужное событие. Чтобы это отобразить в программе, я ввел булевскую переменную run и назначил ей значение истины. Затем добавил цикл, который будет работать до тех пор, пока run имеет такое значение. В этом цикле просматриваю всю очередь событий и при получении события о выходе назначаю переменной run значение ложно, соответственно цикл прекратится и мы выйдем из программы. В результате, если мы запустим программу, то увидим окно, которое будет присутствовать на экране до тех пор, пока мы не выйдем из него.

Пока из таблицы 1.3 мы рассмотрели только одно событие – событие на выход. Это маловато будет для дальнейшей работы, так что давайте изучим остальные события для полноценного контроля над своим приложением.

Обработка клавиатуры

Какая же может быть программа или игра без обработки клавиатуры, которая сейчас является неотъемлемой частью компьютера. Когда мы нажимаем клавишу, SDL генерирует на неё событие. Так мы можем узнать, какие клавиши нажимает пользователь.

Скелет OpenGL

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

#include “SDL_opengl.h”

Как было описано раннее, для установки в окне контекста воспроизведения OpenGL при вызове SetVideoMode, нам надо в аргументе указать флаг SDL_OPENGL. Однако это ещё не всё, нам ещё обязательно потребуется SDL_GL_DOUBLEBUFFER. Этот флаг включает двойную буферизацию, необходимую для плавного рисования в OpenGL. Это тоже ещё не все, нам потребуется установить параметры работы OpenGL с помощью процедуры

SDL_GL_SetAttribute ( <флаг>, <значение> ), где флаг может принимать значения из таблицы 1.4. ?з всего там перечисленного для нашей первой программы потребуется только установка режима двойной буферизации.

SDL_GL_RED_SIZE Размер красного компонента цвета буфера экрана в байтах
SDL_GL_GREEN_SIZE Размер зеленого компонента цвета буфера экрана в байтах
SDL_GL_BLUE_SIZE Размер синего компонента цвета буфера экрана в байтах
SDL_GL_ALPHA_SIZE Размер альфа компонента цвета буфера экрана в байтах
SDL_GL_DOUBLEBUFFER 0 или 1, включение или выключение двойной буферизации
SDL_GL_BUFFER_SIZE Размер буфера цвета в битах
SDL_GL_DEPTH_SIZE Размер буфера глубины в битах
SDL_GL_STENCIL_SIZE Размер буфера трафарета в битах
SDL_GL_ACCUM_RED_SIZE Размер красного компонента цвета буфера накопления в байтах
SDL_GL_ACCUM_GREEN_SIZE Размер зеленого компонента цвета буфера накопления в байтах
SDL_GL_ACCUM_BLUE_SIZE Размер синего компонента цвета буфера накопления в байтах
SDL_GL_ACCUM_ALPHA_SIZE Размер альфа компонента цвета буфера накопления в байтах

Таблица 1.4

Пока что из всех параметров в таблице 1.4 мы будем использовать только задание режима двойной буферизации, об остальном будет сказано поздже.

Теперь давайте добавим в программу процедуру ReDraw, она будет вызываться для рисования очередного кадра. Причем вызывать её будем в нашем главном зацикливании после обработки очереди сообщений

While (run) {

//— обработка очереди событий

While ( PollEvent( &event ) {

}

ReDraw;

}

Чтобы нарисовать очередной кадр с помощью OpenGL, нам нужно сначала очистеть буфер цвета. Это можно сделать с помощью

GlClear ( <флаги> ), где флаги могут принимать значения из 1.5, но нам потребуется только GL_COLOR_BUFFER_BIT. Остальное понадобиться чуть-чуть позднее.

GL_COLOR_BUFFER_BIT Буфер цвета
GL_DEPTH_BUFFER_BIT Буфер глубины
GL_ACCUM_BUFFER_BIT Буфер накопления
GL_STENCIL_BUFFER_BIT Буфер трафарета

Таблица 1.5

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

GlClearColor (<красный компонент цвета>,<зеленый компонент цвета>,<синий компонент цвета>,<альфа компонент цвета>), где мы задаём цвет в формате RGBA (Чтобы задать серый цвет, введите (0.5f, 0.5f, 0.5f, 1.0f) ). Задавать цвет нужно в диапазон от нуля до единицы включительно.

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

SDL_GL_SwapBuffers( );

Пример программы с подключением OpenGL находится в Ex04 и листинге 1.3 я привожу главный куски нашей программы.

//— инициализация

SDL_GL_SetAttribute (SDL_GL_DOUBLEBUFFER, 1);

screen = SDL_SetVideoMode(640, 480, 16, SDL_OPENGL);

//— главный цикл

void ReDraw() {

//— очищаем буфер цвета

glClearColor(0.5f, 0.5f, 0.5f, 1.0f);

glClear (GL_COLOR_BUFFER_BIT);

//— смена буферов

SDL_GL_SwapBuffers();

}

листинг 1.3

Видите, вот и весь скелет OpenGL приложения. С помощью SDL довольно легко всё инициализировать.

Обработка клавиатуры и мыши

Перед тем, как переходить к созданию полноэкранных приложений, давайте освоим технологию обработки действий, связанных с клавиатурой и мышью (нажатие клавиш и т.д.). ?наче, если создадим полноэкранное приложение, то мы не сможем из него выйти. Чтобы исключить такую неприятную ситуацию можно при нажатии ESC организовать завершение программы.

Всю что связанно с клавиатурой и мышью выводится в виде событий, которые SDL для нас добавляет в очередь со всеми остальными. Пока мы обрабатывали только событие выхода из приложения, теперь научимся принимать и читать параметры события клавиатуры.

Событие носит метку SDL_KEYDOWN/UP, а параметры относящиеся к этому событию заносятся в переменную key. Причем структура этой переменной следующая

               typedef struct{
                       Uint8 type;
                       Uint8 state;
                       SDL_keysym keysym;
               } SDL_KeyboardEvent;

где type сообщает нам о типе события – событие на нажатие (SDL_KEYDOWN) или на отжатие кнопки (SDL_KEYUP), state сообщает нам состояние клавиши. Это тоже самое, что и type, можно даже назвать state аналогом, т.е. SDL_RELEASED то же самое, что и SDL_KEYUP, а SDL_PRESSED – SDL_KEYDOWN. Переменная keysym сообщит нам о том, из-за какой же всё-таки клавиши было сгенерировано это событие. Это для нас самое главное.

Полноэкранные приложения

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

SDL_ListModes (<формат пикселей>, <флаги>), возвращает указатель на массив со всеми возможными разрешениями с данным форматом пикселей и с данными флагами из таблицы 1.1. Расширения сортируются от большого к малому.

Что же касается формата пикселей, то мы будем указывать его значение как NULL, тогда формат пикселей будет соответствовать формату пикселей видео устройства, который можно получить самим с помощью вызова функции:

SDL_GetVideoInfo(), которая возвращает информацию о возможностях системы в виде структуры SDL_VideoInfo (я привожу в ней только нужные параметры):

               typedef struct{
                       //-- можно ли создавать поверхности в видео              памяти
                       Uint32 hw_available:1;
                       …
                       //-- общее количество видео памяти
                       Uint32 video_mem;
                       //-- формат пикселей видео устройства
                       SDL_PixelFormat *vfmt;
               } SDL_VideoInfo;

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

//— массив для хранения возможных режимов

SDL_Rect **modes;

Void EnumVideoModes()

{

               int i;
               //-- получим список режимов для полноэкранного
               //--приложения с использованием OpenGL
               modes=SDL_ListModes(NULL, SDL_FULLSCREEN|SDL_OPENGL);
               //-- записываем в наш Log файл возможные режимы
               printf("Available Modes\n");
               for(i=0;modes[i];++i)
                       printf("  %d x %d\n", modes[i]->w, modes[i]->h);
            }

Теперь если мы захотим сменить разрешение экрана, проверим наши потребности с возможностями. Добавим всё это для создания программы, работающей в полноэкранном режиме. В ex.05 находится такая программа, в листинге 1.4 я привожу описание основных частей.

//— размеры экрана вводим как константы

//— так будет легко их менять

#define SCREEN_WIDTH              800

#define SCREEN_HEIGHT            600

//— проверка на поддержку данного режима

//— возвращает 0 в случае провала

int CheckVideoModeSupport()

{

int i;

for (i=0;modes[i];++i)

if (modes[i]->w == SCREEN_WIDTH &&

modes[i]->h == SCREEN_HEIGHT) return 1;

return 0;

}

//— заносим в массив все возможные режимы

EnumVideoModes();

//— проверяем на возможность установки данного

if (!CheckVideoModeSupport()) {

fprintf(stderr, «Невозможно установить разрешение %dx%d\n»,

SCREEN_WIDTH, SCREEN_HEIGHT);

exit(-1);

}

//— создаем окно в полноэкранном режиме

screen = SDL_SetVideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_BPP,

SDL_FULLSCREEN | SDL_OPENGL);

листинг 1.4

OpenGL вне зависимости от платформы или SDL
Метки:    

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

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