Урок 35. Проигрывание AVI файлов в OpenGL.
Я хотел бы начать с того, что я очень горжусь своей обучающей программой. Когда Джонатан де Блок подкинул мне идею сделать AVI проигрыватель в OpenGL, я не имел понятия о том, как открыть видео-файл, не говоря уже о проигрывателе их. Я начал с того, что заглянул в мою коллекцию книг по программированию. Но не в одной из них не было информации об AVI. Тогда я начал читать все, что можно было прочитать об AVI в MSDN. В MSDN много полезной информации, но мне нужно было больше.
После нескольких часов поиска примеров проигрывания AVI, я нашел два сайта. Я не хочу сказать, что я великолепный следопыт, но я обычно всегда нахожу, что ищу. Я был неприятно поражен, когда я увидел, как немного примеров было в Web. Большинство файлов, которые я нашел, не компилировались. Часть примеров была сложна (по крайней мере, для меня), и они делали то, что надо, но исходники были написаны на VB, Delphi, и т.д. (но не VC++).
Вначале отметим статью, написанную Джонатаном Никсом и озаглавленную "Работа с AVI файлами" ("Working with AVI Files"). Вы можете найти ее по адресу: http://www.gamedev.net/reference/programming/features/avifile/
Я очень уважаю Джонатана за написание такой блестящей статьи про формат AVI. Хотя я решил написать код по-другому, отрывки из его кода и комментарии сделали процесс обучения намного проще! Вторым сайтом был "Краткий обзор по AVI" ("The AVI Overview") Джона Ф. МакГоуана. Я мог бы рассказывать и рассказывать об этой удивительной странице Джона, но проще, если вы сами посмотрите на нее. Вот адрес:
http://www.jmcgowan.com/avi.html
Его сайт в значительной степени покрывает все, что нужно знать об AVI формате. Спасибо Джону за создание такой замечательной и доступной всем страницы.
Последнее, что я хотел упомянуть то, что НЕ ОДИН отрывок кода не был скопирован или заимствован. Весь код к уроку был написан за три дня, используя информацию с указанных сайтов и статей. Я хотел бы обратить ваше внимание на то, что этот код не является ЛУЧШИМ способом проигрывания видео-файлов. Это может быть даже неправильный способ проигрывания AVI файлов. Если вам не нравится мой код, или мой стиль программирования, или вы чувствуете, что я порчу программистское сообщество, выпуская этот урок, у вас есть несколько вариантов: 1) Поискать в сети альтернативные статьи.
2) Написать ваш собственный AVI проигрыватель.
3) Или написать ваш собственный урок!
Каждый, кто посетит мой сайт должен знать, что я средний программист со средними способностями (я упоминал об этом на многочисленных страницах этого сайта)! Я кодирую ради ЗАБАВЫ! Цель этого сайта в том, чтобы сделать жизнь проще для начинающих программистов, которые начинают изучение OpenGL. Эти уроки просто примеры того, как сделать тот или иной эффект. Не больше и не меньше!
Приступим…
Первое что вы заметите это то, что мы подключили библиотеку Видео для Windows. Большое спасибо Microsoft (я не могу поверить, что только сказал это!). Эта библиотека открывает и проигрывает AVI файлы. Всё, что вам надо знать это то, что вы ДОЛЖНЫ подключить файл vfw.h и прилинковать vfw32.lib, если вы хотите, чтобы этот код скомпилировался.
#include <windows.h> // Заголовочный файл Windows
#include <gl\gl.h> // Заголовочный файл библиотеки OpenGL32
#include <gl\glu.h> // Заголовочный файл библиотеки Glu32
#include <vfw.h> // Заголовочный файл для «Видео для Windows»
#include "NeHeGL.h" // Заголовочный файл NeHeGL.h
#pragma comment( lib, "opengl32.lib" ) // Искать OpenGL32.lib при линковке
#pragma comment( lib, "glu32.lib" ) // Искать GLu32.lib при линковке
#pragma comment( lib, "vfw32.lib" ) // Искать VFW32.lib при линковке
#ifndef CDS_FULLSCREEN // CDS_FULLSCREEN не определяется некоторыми
#define CDS_FULLSCREEN 4 // компиляторами. Определяем эту константу
#endif // Таким образом мы можем избежать ошибок
GL_Window* g_window;
Keys* g_keys;
Теперь мы объявим переменные. Переменная angle используется, для того чтобы вращать объекты, основываясь на прошедшем времени. Мы будем использовать эту переменную везде, где есть вращение для простоты.
Next — целая переменная которая будет использована для того чтобы узнать сколько времени прошло (в миллисекундах). Она будет использована для сохранения начальной частоты кадров. Мы поговорим об этом позже.
Переменная frame…Конечно это текущий кадр анимации, который мы хотим отобразить. Мы начинаем с 0 (первый кадр). Я думаю безопасно (для программы) предположить что видео, которое мы открыли ДОЛЖНО ИМЕТЬ хотя бы один кадр :).
Переменная effect текущий эффект видимый на экране (объекты: Куб, Сфера, Цилиндр, Ничто). Env - булевская переменная. Если она равна Истине, то тогда наложение окружения включено, если Ложь, то окружение не будет отображено. Если bg Истина, то вы будете видеть полноэкранное видео за объектом. Если Ложь, вы будете видеть только объект (никакого фона).
sp, ep and bp используются чтобы быть уверенным в том, что пользователь не удерживает нажатой соответствующую клавишу.
// Пользовательские переменные
float angle; // Для вращения
int next; // Для анимации
int frame=0; // Счётчик кадров
int effect; // Текущий эффект
bool sp; // Пробел нажат?
bool env=TRUE; // Показ среды
(По умолчанию включен)
bool ep; // 'E' нажато?
bool bg=TRUE; // Фон(по умолчанию включен)
bool bp; // 'B' нажато?
В структуре psi будет сохранена информация о нашем AVI файле далее в коде. pavi — указатель на буфер, который получает новый дескриптор потока, как только AVI файл будет открыт. pgf - это указатель на объект GetFrame. bmih будет использована потом в коде для конвертирования кадра анимации в формат, который мы захотим (содержит заголовок растра, описывающий, что мы хотим). Lastframe будет содержать номер последнего файла AVI анимации. width и height будут содержать размеры видео потока и наконец… pdata будет указателем на содержимое изображения возвращенного после получения кадра анимации из AVI! Mpf будет использован для подсчёта, сколько миллисекунд каждый кадр отображается на экране. Мы поговорим об этом чуть позже.
AVISTREAMINFO psi; // Указатель на структуру содержащую информацию о потоке
PAVISTREAM pavi; // Дескриптор для открытия потока
PGETFRAME pgf; // Указатель на объект GetFrame
BITMAPINFOHEADER bmih; // Заголовочная информация для DrawDibDraw декодирования
long lastframe; // Последний кадр анимации
int width; // Ширина видео
int height; // Высота видео
char *pdata; // Указатель на данные текстуры
int mpf; // Сколько миллисекунд отображен кадр
В этом уроке мы создадим 2 разных квадратичных объекта (сферу и цилиндр) используя библиотеку GLU. Переменная quadratic - это указатель на наши квадратичные объекты.
hdd - это дескриптор контекста устройства DrawDib . hdc - дескриптор контекста устройства.
hBitmap - это дескриптор устройства вывода аппаратно-независимого растра (будет использован потом в процессе конвертирования растра).
data - указатель который укажет на данные нашего конвертированного изображения. Будет иметь смысл позже. Продолжайте читать :).
GLUquadricObj *quadratic; // Хранилище для наших квадратичных объектов
HDRAWDIB hdd; // Дескриптор для нашего рисунка
HBITMAP hBitmap; // Дескриптор устройства растра
HDC hdc = CreateCompatibleDC(0); // Создание совместимого контекста устройства
unsigned char* data = 0; // Указатель на наше измененное в размерах изображение
Теперь немного ассемблера. Тем из вас, которые до этого никогда не пользовались ассемблером, не стоит бояться. Это может выглядеть загадочно, но это просто!
Пока я сочинял этот урок, я обнаружил весьма большую проблему. Первое видео, которое я получил, проигрывалось прекрасно, но при этом цвета отображались неверно. Везде, где должен быть красный цвет был синий, а где должен быть синий был красный. Я начал СХОДИТЬ С УМА. Я был убежден, что я сделал, где-то ошибку в коде. После просмотра всего кода, я не смог найти ошибку! Тогда я начал снова читать MSDN. Почему красные и голубые байты переставлены местами!? В MSDN говорилось, что 24 битные изображения в формате RGB!!! После более углубленного изучения я нашел, в чем состоит проблема. В Windows данные RGB (в картинках) фактически хранятся наоборот (BGR). В OpenGL, RGB это по настоящему… RGB.
После нескольких жалоб от фанатов Microsoft’а :) я решил добавить небольшое замечание! Я не ругаю Microsoft за то, что RGB данные сохраняются наоборот. Мне только очень непонятно, когда-то, что называется RGB в действительности является BGR!
Техническая заметка: есть “little endian” стандарт, а есть “big endian”. Intel и аналоги Intel используют “little endian”, где наименее значимый байт (LSB) идет первым. OpenGL пришел из SGI, где распространен «big endian», и OpenGL требует растровый формат в своем стандарте.
Замечательно! Так вот я тут с проигрывателем, который напоминает мне абсолютное дерьмо! Моим первым решением было поменять байты вручную в следующем цикле. Это работало, но очень медленно. Сытый по горло, я модифицировал код генерации текстур, чтобы он использовал GL_BGR_EXT вместо GL_RGB. Огромное увеличение скорости и цвета, все выглядело прекрасно! Так моя проблема была решена… или я так думал. Некоторые OpenGL драйверы имели проблемы с GL_BGR_EXT… . Назад к рисовальной доске :(.
После разговора с моим хорошим другом Максвелом Сэйлом, он посоветовал мне поменять местами байты, используя asm-код. Минуту позже был готов код, который я привел ниже! Может быть он не оптимизирован, но он работает и быстро работает!
Каждый кадр анимации сохраняется в буфере. Рисунок всегда будет иметь 256 пикселей в ширину, 256 пикселей в высоту и 1 байт на цвет (3 байта на пиксель). Код ниже будет проходить по буферу, и переставлять красные и синие байты. Красный хранится в ebx+0, а голубой в ebx+2. Мы двигаемся через буфер, обрабатывая по три байта за раз (пиксель состоит из трёх байтов). Мы будем делать это, пока все данные не будут переставлены.
У некоторых из вас были проблемы с использованием ASM кода, я полагаю, что я объясню, почему я использую его в своем уроке. Сначала я планировал использовать GL_BGR_EXT поскольку это работает. Но не на всех платах! Тогда я решил использовать метод перестановки с последнего урока (очень опрятный код перестановки методом XOR). Перестановка работала на всех машинах, но она не была быстрой. В прошлом уроке это хорошо работало. Но сейчас мы имеем дело с ВИДЕО В РЕАЛЬНОМ ВРЕМЕНИ. Вы хотите иметь самую быструю перестановку. Взвесив всё, ASM по моему мнению наилучший выбор!
Если у вас есть лучший путь чтобы сделать эту работу, пожалуйста… ИСПОЛЬЗУЙТЕ ЕГО! Я не говорю вам, как делать эти вещи. Я показываю, как я сделал это. Я также подробно объясняю свой код. Если вы хотите написать лучший код, то вы знаете, как устроен мой код, сделайте его проще и найдите альтернативный метод, если вы хотите написать ваш код!
void flipIt(void* buffer) // Функция меняющая красный и синий цвет
{
void* b = buffer; // Указатель на буфер
__asm // Начало asm кода
{
mov ecx, 256*256 // Установка счётчика (Размер блока памяти)
mov ebx, b // Указатель ebx на наши данные (b)
label: // Метка для цикла
mov al,[ebx+0] // Загружаем значение из ebx в регистр al
mov ah,[ebx+2] // Загружаем значение из ebx+2 в регистр ah
mov [ebx+2],al // Сохраняем данные в al из ebx+2
mov [ebx+0],ah // Сохраняем данные в ah из ebx
add ebx,3 // Перемещаем указатель на три байта
dec ecx // Уменьшаем наш счётчик
jnz label // Если не равно нулю перемещаемся назад
}
}
Код ниже открывает AVI файл в режиме чтения. SzFile - это название файла который мы хотим открыть. title[100] будет использован чтобы модифицировать заголовок окна (чтобы показать информацию об AVI файле).
Первое что нам надо сделать это вызвать AVIFileInit(). Она инициализирует библиотеку по работе с файлами AVI.
Есть много способов, чтобы открыть AVI файл. Я решил использовать функцию AVIStreamOpenFromFile(…). Она открывает единственный поток из AVI файла (AVI файлы могут содержать несколько потоков).
Параметры следующие: pavi - это указатель на буфер, который получает новый дескриптор потока. szFile - это имя файла, который мы желаем открыть (полный путь). Третий параметр - это тип потока. В нашей программе мы заинтересованы только в видео потоке (streamtypeVIDEO). Четвертый параметр обозначает номер потока (мы хотим только первый). OF_READ обозначает то, что мы хотим открыть файл ТОЛЬКО для чтения. Последний параметр - это указатель на класс идентификатора дескриптора, который мы хотим использовать. Если честно, то я не знаю для чего он. Позволим Windows выбрать его, послав NULL в последнем параметре.
Если возникнут, какие-то ошибки при открытии файла, то выскочит окно и даст вам знать о том, что поток не может быть открыт. Я не сделал так, чтобы при этой ошибке программа вызывала какую-то секцию кода. Если будет ошибка, программа будет пробовать проиграть файл. Добавление проверки потребовало бы усилий, а я очень ленивый :).
void OpenAVI(LPCSTR szFile) // Вскрытие AVI файла (szFile)
{
TCHAR title[100]; // Будет содержать заголовок
AVIFileInit(); // Открывает файл
// Открытие AVI потока
if (AVIStreamOpenFromFile(&pavi, szFile, streamtypeVIDEO, 0, OF_READ, NULL) !=0)
{
// Если ошибка
MessageBox (HWND_DESKTOP, "Failed To Open The AVI Stream",
"Error", MB_OK | MB_ICONEXCLAMATION);
}
Если мы сделали это, то можно считать что файл был открыт и данные потока локализованы! После этого мы получаем немного информации от AVI файла с помощью AVIStreamInfo(…).
Ранее мы создали структуру psi, которая будет содержать информацию о нашем AVI потоке. Эта структура заполнится информацией с помощью первой строки кода ниже. Всё от ширины потока (в пикселях) до частоты кадров анимации сохранено в psi. Для тех, кто хочет добиться точной скорости воспроизведения сделайте, как я сказал. Более подробную информацию ищите об AVIStreamInfo в MSDN.
Мы можем вычислить ширину кадра отняв левую границу окна из правой. Это будет точная ширина в пикселях. Высота кадра получается, когда мы вычитаем верхнюю границу из нижней. Это даст нам высоту в пикселях.
Затем мы находим номер последнего кадра из AVI файла используя AVIStreamLength(…). Она возвращает число кадров анимации в AVI файле. Результат сохранен в lastframe.
Вычисление частоты кадров довольно просто. Кадры в секунду = psi.dwRate / psi.dwScale. Это значение совпадает со значением, которое можно получить в свойствах AVI-файла, если щелкнуть по нему правой кнопкой мыши в Проводнике. Так почему же мы используем mpf спросите вы? Когда я впервые написал этот код, я попробовал использовать этот метод чтобы выбрать правильный кадр анимации. Я столкнулся с проблемой… У меня есть файл face2.avi продолжительностью 3.36 секунды. Частота кадров 29.974 кадров в секунду. Видео имеет 91 кадр. Если вы умножите 3.36 на 29.974 вы получите 100 кадров. Очень странно!
Я решил переписать код немного по-другому. Вместо вычисления частоты кадров в секунду, я посчитал, как долго каждый кадр показывается на экране. AVIStreamSampleToTime() конвертирует позицию анимацию в «сколько миллисекунд требуется чтобы добраться до этой позиции».Таким образом мы вычисляем сколько миллисекунд имеет все видео с помощью получения времени (в миллисекундах) последнего кадра. Тогда мы делим результат на общее количество кадров анимации (lastframe). Это даёт нам время необходимое для показа одного кадра. Мы сохраняем полученный результат в переменной mpf (millisecond per frame — число миллисекунд на кадр). Вы также можете посчитать, сколько отводится миллисекунд на кадр посредством получения времени первого кадра анимации с помощью вот этого кода: AVIStreamSampleToTime(pavi,1). Простой и отлично работающий способ! Большое спасибо Альберту Чаулку за эту идею!
Причина, по которой я говорю приблизительное число миллисекунд на кадр та, что mpf целое и любое дробное значение будет округлено!
AVIStreamInfo(pavi, &psi, sizeof(psi)); // Записываем информацию о потоке в psi
width=psi.rcFrame.right-psi.rcFrame.left; // Ширина = правая граница минус левая
height=psi.rcFrame.bottom-psi.rcFrame.top;// Высота равна верх минус низ
lastframe=AVIStreamLength(pavi); // Последний кадр потока
// Вычисление приблизительных миллисекунд на кадр
mpf=AVIStreamSampleToTime(pavi,lastframe)/lastframe;
Поскольку OpenGL требует, чтобы данные в текстуре были кратны двум, и потому что большинство видео размеров 160x120, 320x240 и некоторые другие странные размеры, нам нужен быстрый способ на лету изменить размеры видео в формат, который мы можем использовать как текстуру. Чтобы сделать это мы воспользуемся преимуществом Windows DIB функций.
Первую вещь, которую мы сделаем это опишем тип нужного нам изображения. Чтобы сделать это мы заполняем структур bmih типа BitmapInfoHeader нужными данными. Мы начнём с изменения размера структуры. Тогда мы установим bitplanes в 1. Три байта данных это 24 битовый цвет (RGB). Мы хотим изображение 256х256 пикселей и, наконец, мы хотим, чтобы данные возвращались как UNCOMPRESSED RGB (BI_RGB).
Функция CreateDIBSection создает изображение, в которое мы и запишем рисунок. Если всё прошло нормально hBitmap укажет нам на значения битов изображения. Hdc - это дескриптор контекста устройства (DC). Второй параметр - это указатель на структуру BitmapInfo. Это структура содержит информацию об изображении как было сказано выше. Третий параметр (DIB_RGB_COLORS) определяет, что данные в формате RGB.data - это указатель на переменную, которая получает указатель на DIB данные. Если мы укажем в качестве пятого параметра NULL, память будет выделена под наш рисунок. Наконец последний параметр может быть игнорирован (установлен в NULL).
Цитата из MSDN: Функция SelectObject выбирает объект для контекста устройства (DC).
Теперь мы создали DIB, который мы можем непосредственно выводить на экран. Вау :).
bmih.biSize = sizeof (BITMAPINFOHEADER); // Размер BitmapInfoHeader’а
bmih.biPlanes = 1; // Размер
bmih.biBitCount = 24; // Формат битов
bmih.biWidth = 256; // Ширина(256 пикселов)
bmih.biHeight = 256; // Высота(256 пикселов)
bmih.biCompression = BI_RGB; // Цветовой режим (RGB)
hBitmap = CreateDIBSection (hdc, (BITMAPINFO*)(&bmih),
DIB_RGB_COLORS, (void**)(&data), NULL, NULL);
SelectObject (hdc, hBitmap) // Выбор hBitmap в наш контекст устройства (hdc)
Ещё несколько вещей мы должны сделать, чтобы быть готовыми к чтению кадров из AVI. Следующее что нам надо сделать, это подготовить нашу программу к извлечению кадров из файла с видео-фильмом. Для этого мы используем AVIStreamGetFrameOpen(…).
Вы можете передавать структуру подобно тому, как мы выше передавали второй параметр, чтобы получить заданный видео формат. К сожалению, единственное, что мы можем изменять при этом ширину и высоту возвращаемого изображения.
Если всё прошло хорошо, объект GETFRAME возвращен (он нужен нам, для того чтобы читать кадры). Если есть какие-нибудь проблемы, окно выскочит и сообщит вам об ошибке.
pgf=AVIStreamGetFrameOpen(pavi, NULL); // Создание PGETFRAME с нужными нам параметрами
if (pgf==NULL)
{
// Если ошибка
MessageBox (HWND_DESKTOP, "Failed To Open The AVI Frame",
"Error", MB_OK | MB_ICONEXCLAMATION);
}
Код ниже выводит ширину, высоту и количество кадров в заголовок. Мы показываем заголовок с помощью функции SetWindowText(…). Запустите программу в оконном режиме, чтобы увидеть, что делает этот код.
// Информация для заголовка (Ширина/Высота/Кол-во кадров)
wsprintf (title, "NeHe's AVI Player: Width: %d, Height: %d, Frames: %d",
width, height, lastframe);
SetWindowText(g_window->hWnd, title); // Изменение заголовка
}
Теперь интересное…мы захватываем кадр из AVI и конвертируем его к используемым размерам изображения и разрядности цвета. lpbi будет содержать информацию BitmapInfoHeader для кадра анимации. Во второй строчке кода мы выполняем сразу несколько вещей. Сначала мы захватываем кадр анимации. Кадр, который мы хотим, задан frame. Это считает кадр анимации и заполнит lpbi информацией для этого кадра.
Еще интересного … нам необходим указатель на данные изображения. Чтобы сделать это мы должны опустить информацию заголовка (lpbi->biSize). Одну вещь я не делал пока не сел писать этот урок. Она состоит в том, что мы должны также опустить любую информацию о цвете. Чтобы сделать это мы должны сложить цвета, умноженные на размер RGBQUAD (biClrUsed*sizeof(RGBQUAD)). После выполнения ВСЕГО, что мы хотели :) мы оставлены один на один с указателем на данные (pdata).
Сейчас нам надо конвертировать кадр анимации к размеру используемой текстуры также, мы должны преобразовать данные в RGB формат. Чтобы сделать это мы используем DrawDibDraw(…).
Краткое замечание. Мы можем рисовать непосредственно в наш DIB. Это делает DrawDibDraw(…). Первый параметр - это дескриптор нашего DrawDib DC. Второй - это дескриптор на наш DC. Следующие параметры - это верхний левый угол (0,0) и правый нижний угол (256,256) результирующего прямоугольника.
lpbi — указатель на BitmapInfoHeader информацию для кадра который мы сейчас читаем. pdata — указатель на данные изображения для этого кадра.
Теперь у нас есть верхний левый угол (0,0) исходного изображения (текущий кадр) и правый нижний угол кадра (ширина и высота кадра). Последний параметр пусть будет нуль.
Таким образом, мы преобразуем изображение любого размера и разрядности цвета к 256*256*24.
void GrabAVIFrame(int frame) // Захват кадра
{
LPBITMAPINFOHEADER lpbi; // Содержит BitmapInfoHeader
// Получение данных из потока
lpbi = (LPBITMAPINFOHEADER)AVIStreamGetFrame(pgf, frame);
// Указатель на данные возвращенные AVIStreamGetFrame
// (Пропуск заголовка для получения указателя на данные)
pdata=(char *)lpbi+lpbi->biSize+lpbi->biClrUsed * sizeof(RGBQUAD);
// Преобразование информации в нужный нам формат
DrawDibDraw (hdd, hdc, 0, 0, 256, 256, lpbi, pdata, 0, 0, width, height, 0);
Теперь у нас есть наш кадр анимации, но красные и голубые байты переставлены. Чтобы решить эту проблему мы переходим к нашему быстрому flipIt(…) коду. Помните, data — это указатель на переменную, которая получает указатель на расположения битовых значений DIB’а. Это означает то, что после того как мы вызовем DrawDibDraw, data укажет на наши изменённые (256*256*24) растровые данные.
Первоначально я создавал текстуру для каждого кадра анимации. Я получил несколько писем предлагающих мне использовать glTexSubImage2D(). После чтения «Красной книги по OpenGL», я наткнулся на следующую цитату: «Создание текстуры может быть в вычислительном отношении более дорогостоящим, чем изменить существующую. В OpenGL версии 1.1 есть подпрограммы для замены всей площади или части текстуры на новую информацию. Это может быть полезно для некоторых приложений, которые делаю анимацию в реальном времени, и захвата видео изображений в текстуры. Для этих приложений имеет смысл создавать одну текстуру и использовать glTexSubImage2D() чтобы потом неоднократно заменять данные текстуры новыми видео изображениями».
Лично я не замечал огромного увеличения скорости, но если у вас слабая видеокарта вы могли бы стать свидетелем этого. Параметры для glTexSubImage2D() следующие: наша цель - двухмерная текстура (GL_TEXTURE_2D). Уровень детализации (0), который используется для мипмэпинга. Смещения x(0) и y(0) которые показывают функции, где начать копирование (0,0 нижний левый угол текстуры). У нас есть размеры изображения, которое мы хотим копировать (256*256). GL_RGB - это формат данных. Мы копируем беззнаковые данные. Очень просто.
Заметка Кевина Рогерса: я только хотел указать на другую причину использования glTexSubImage2D. Это не только будет быстрее для большинства приложений OpenGL, но целевая область может быть по размеру не обязательно кратной степени 2. Это особенно удобно для воспроизведения, так как типичные размеры кадра редко кратные степени 2 (часто 320*200 или подобные). Это даёт вам достаточную гибкость, чтобы запустить видео поток в его первоначальном варианте, чем искажать и отсекать каждый кадр, для того чтобы приспособить его к вашим размерам.
Важно обратить ваше на то, что вы не можете обновлять текстуру, если вы до этого её не создали! Мы создаем текстуру в Initialize().
Я также хотел упомянуть… Если вы планируете использовать больше одной текстуры в вашем проекте удостоверьтесь, что вы связываете текстуру (glBindTexture()) . Если вы не свяжете текстуру она не будет обновлена!
flipIt(data); // Перестановка красных и синих байтов
// Обновление текстуры
glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, 256, 256, GL_RGB, GL_UNSIGNED_BYTE, data);
}
Следующая секция кода вызывается, когда программа завершается. Мы закрываем DC, и ресурсы освобождаются. Тогда мы разблокируем ресурсы AVI GetFrame. Наконец мы завершаем поток и закрываем файл.
void CloseAVI(void) // Функция закрытия
{
DeleteObject(hBitmap); // Уничтожение устройства растра
DrawDibClose(hdd); // Закрытие контекста DrawDib устройства
AVIStreamGetFrameClose(pgf); // Закрытие объекта GetFrame
AVIStreamRelease(pavi); // Завершение потока
AVIFileExit(); // Закрытие файла
}
Инициализация довольно проста. Мы устанавливаем угол в 0. Далее мы открываем DrawDib библиотеку (которая получает DC). Если все хорошо, hdd становится дескриптором на только что созданный контекст устройства.
Наш экран черный, включается тестирование глубины, и т.д..
Затем мы создаем новый квадратичный объект. quadratic - это указатель на наш новый объект. Мы устанавливаем сглаженные нормали, и включаем автогенерацию текстурных координат для нашего квадратичного объекта.
BOOL Initialize (GL_Window* window, Keys* keys) //Инициализация
{
g_window = window;
g_keys = keys;
// Начало инициализации
angle = 0.0f; // Установка угла в ноль
hdd = DrawDibOpen(); // Получение контекста устройства
glClearColor (0.0f, 0.0f, 0.0f, 0.5f); // Черный фон
glClearDepth (1.0f); // Установка буфера глубины
glDepthFunc (GL_LEQUAL); // Тип тестирования глубины (Less или Equal)
glEnable(GL_DEPTH_TEST); // Включение теста глубины
glShadeModel (GL_SMOOTH); // Выбор гладкости
// Очень аккуратная установка перспективы
glHint (GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
quadratic=gluNewQuadric(); // Создание нового квадратичного объекта
gluQuadricNormals(quadratic, GLU_SMOOTH); // Сглаженные нормали
gluQuadricTexture(quadratic, GL_TRUE); // Создание текстурных координат
В следующем кусочке кода, мы включаем отображение двухмерных текстур, и мы устанавливаем фильтры текстур в GL_NEAREST (быстро, но грубо) и мы устанавливаем сферическое наложение (чтобы создать эффект наложения окружения). Проиграйтесь с фильтрами. Если у вас мощный компьютер, попробуйте GL_LINEAR для более гладкой анимации.
После установки нашей текстуры и сферического наложения мы открываем .AVI файл. Файл называется face2.avi и он расположен в каталоге 'data'.
Последнее, что мы должны сделать — это создать нашу первоначальную текстуру. Мы должны сделать это чтобы использовать glTexSubImage2D() для модификации нашей текстуры в GrabAVIFrame().
glEnable(GL_TEXTURE_2D); // Включение двухмерных текстур
// Установка фильтра увеличения текстуры
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
// Установка фильтра уменьшения текстуры
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
// Включение автогенерации текстурных координат по координате S сферического наложения
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
// Включение автогенерации текстурных координат по координате T сферического наложения
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
OpenAVI("data/face2.avi"); // Откроем видео-файл
// Создание текстуры
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 256, 256, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
return TRUE; //Возвращение true (инициализация успешна)
}
При завершении мы вызываем CloseAVI(). Это корректно закроет AVI файл и все используемые ресурсы.
void Deinitialize (void) //Вся деиницилизация здесь
{
CloseAVI(); // Закрываем AVI
}
Далее мы проверяем клавиши и обновляем наше вращение (angle) относительно прошедшего времени. Сейчас я не буду подробно объяснять код. Мы проверяем, нажат ли пробел. Если это так, то мы выполняем следующий по списку эффект. У нас есть три эффекта (куб, сфера, цилиндр) и когда выбран четвёртый эффект (effect=3) ничего не рисуется…показывается лишь сцена! Когда выбран четвёртый эффект и нажат пробел, то мы возвращаемся к первому эффекту (effect = 0). Да, я знаю, я должен был назвать это ОБЬЕКТОМ :).
Затем мы проверяем, нажата ли клавиша ‘B’ если это так, то мы переключаем фон (bg) от включенного состояния в выключенное или наоборот.
Для отображения окружения мы проделаем то же самое. Мы проверяем, нажата ли ‘E’. Если это так, то мы переключаем env от TRUE к FALSE и наоборот. То есть, включено наложение окружения или нет.
Угол увеличивается на крошечную долю каждый раз при вызове Update(). Я делю время на 60.0f, чтобы немного замедлить скорость вращения.
void Update (DWORD milliseconds) // Движение обновляется тут
{
if (g_keys->keyDown [VK_ESCAPE] == TRUE) // Если ESC нажат
{
TerminateApplication (g_window); // Завершение приложения
}
if (g_keys->keyDown [VK_F1] == TRUE) // Если F1 нажата
{
ToggleFullscreen (g_window); // Включение полноэкранного режима
}
if ((g_keys->keyDown [' ']) && !sp) // Пробел нажат и не удерживается
{
sp=TRUE; // Установка sp в истину
effect++; // Изменение эффекта (увеличение effect)
if (effect>3) // Превышен лимит?
effect=0; // Возвращаемся к нулю
}
if (!g_keys->keyDown[' ']) // Если пробел отпущен
sp=FALSE; // Установка sp в False
if ((g_keys->keyDown ['B']) && !bp) // ‘B’ нажат и не удерживается
{
bp=TRUE; // Установка bp в True
bg=!bg; // Включение фона Off/On
}
if (!g_keys->keyDown['B']) // Если ‘B’ отпущен
bp=FALSE; //Установка bp в False
if ((g_keys->keyDown ['E']) && !ep) // Если ‘E’ нажат и не удерживается
{
ep=TRUE; // Установка ep в True
env=!env; // Включение отображения среды Off/On
}
if (!g_keys->keyDown['E']) // Если 'E' отпущен?
ep=FALSE; // Установка ep в False
angle += (float)(milliseconds) / 60.0f; // Обновление angle на основе времени
В первоначальном варианте урока, все AVI файлы проигрывались с одинаковой скоростью. После этого программа была переписана, чтобы запустить видео с правильной скоростью. next - увеличивает число миллисекунд, которое прошло после вызова этой секции кода в последний раз. Если ранее в уроке мы вычисляли, как долго каждый кадр должен быть отображен (mpf). Чтобы вычислить текущий кадр, мы берём прошедшее время и делим его на mpf.
После этого нам надо удостовериться в том, что номер текущего кадра анимации не больше общего числа кадров. Если это так, то анимация будет сброшена в нуль и начата заново.
Код ниже пропустит кадры, если ваш компьютер тормозит или другое приложение занимает процессор. Если вы хотите, чтобы каждый кадр был отображен независимо от того тормозит ли компьютер, вы можете проверить является ли next больше mpf и, если это так, то сбросьте next и увеличьте frame на единицу. Любой способ сработает, но код ниже больше подходит для более мощных машин.
Если вы чувствуете силы, попробуйте добавить перемотку, быструю перемотку, паузу или обратный ход проигрывания!
next+= milliseconds; // Увеличение next основанное на таймере (миллисекундах)
frame=next/mpf; // Вычисление текущего кадра
if (frame>=lastframe) // Не пропустили ли мы последний кадр?
{
frame=0; // Сбрасываем frame назад в нуль (начало анимации)
next=0; // Сбрасываем таймер анимации (next)
}
Теперь код рисования. Мы очищаем буфер глубины и экрана. Затем мы получаем кадр анимации. Снова, я постараюсь сделать код простым!
Вы передаете требуемый кадр (frame) функции GrabAVIFrame(). Довольно просто! Конечно, если бы хотели воспроизводить многопотоковый AVI вы должны были бы передать текстурный идентификатор.
void Draw (void) // Прорисовка сцены
{
// Очистка экрана и буфера глубины
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
GrabAVIFrame(frame); // Захват кадра анимации
Код ниже проверяет, хотим ли мы видеть фоновое изображение. Если bg равен TRUE, мы сбрасываем матрицу и прорисовываем одну текстуру в форме квадрата (отображает кадр AVI) достаточно большую чтобы заполнить экран. Квадрат нарисован на 20 единиц вглубь экрана (-20), поэтому он всегда показывается позади объекта.
if (bg) // Фоновое изображение показывать?
{
glLoadIdentity(); // Сброс матрицы просмотра
glBegin(GL_QUADS); // Начало прорисовки фонового рисунка
// Передняя грань
glTexCoord2f(1.0f, 1.0f); glVertex3f( 11.0f, 8.3f, -20.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-11.0f, 8.3f, -20.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-11.0f, -8.3f, -20.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 11.0f, -8.3f, -20.0f);
glEnd(); // Конец рисования
}
После прорисовки фона (или не прорисовки), мы сбрасываем матрицу (это переводит нас в центр экрана по всем трём измерениям). Далее мы сдвигаем матрицу на 10 единиц вглубь экрана (-10).
После этого мы проверяем, равна ли переменная env значению TRUE. Если это так, то мы включаем сферическое наложение для создания эффекта наложения окружения.
glLoadIdentity (); // Сброс матрицы
glTranslatef (0.0f, 0.0f, -10.0f); // На десять единиц в экран
if (env) // Включено отображение эффектов
{
glEnable(GL_TEXTURE_GEN_S); // Вкл. автогенерация координат текстуры по S (Новое)
glEnable(GL_TEXTURE_GEN_T); // Вкл. автогенерация координат текстуры по T (Новое)
}
Я добавил следующий код в последнюю минуту. Он вращает сцену по оси x и оси y (основываясь на значении angle) и, наконец, сдвигает сцену на две единицы по оси z. Это переместит все вглубь экрана. Если вы удалите эти три строки кода ниже, объект будет просто крутиться в середине экрана. С этими тремя строками, объекты будут немного двигаться и одновременно вращаться :).
Если вы не понимаете, как делается вращение и передвижение… этот урок для вас слишком сложный :).
glRotatef(angle*2.3f,1.0f,0.0f,0.0f); // Немного вращает объекты по оси x
glRotatef(angle*1.8f,0.0f,1.0f,0.0f); // Делает то же самое только по оси y
glTranslatef(0.0f,0.0f,2.0f); // После вращения перемещение
Код ниже проверяет, какой из эффектов мы хотим прорисовать. Если значение effect равно 0, мы делаем небольшое вращение и рисуем куб. Поворот вращает куб по x,y и z осям. К настоящему времени вы должны иметь код, чтобы создать куб, родивший в вашей голове :).
switch (effect) // Какой эффект?
{
case 0: // Эффект 0 - Куб
glRotatef (angle*1.3f, 1.0f, 0.0f, 0.0f); // Вращение по оси x
glRotatef (angle*1.1f, 0.0f, 1.0f, 0.0f); // Вращение по оси y
glRotatef (angle*1.2f, 0.0f, 0.0f, 1.0f); // Вращение по оси z
glBegin(GL_QUADS); // Начало рисования куба
//Передняя грань
glNormal3f( 0.0f, 0.0f, 0.5f);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
//Задняя грань
glNormal3f( 0.0f, 0.0f,-0.5f);
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
//Верхняя грань
glNormal3f( 0.0f, 0.5f, 0.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
// Нижняя грань
glNormal3f( 0.0f,-0.5f, 0.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
// Правая грань
glNormal3f( 0.5f, 0.0f, 0.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f);
// Левая грань
glNormal3f(-0.5f, 0.0f, 0.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f);
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f);
glEnd(); // Конец рисования нашего куба
break; // Конец нулевого эффекта
Теперь мы выводим сферу. Мы начинаем с небольшого вращения по всем осям. Далее мы рисуем сферу. Сфера будет иметь радиус 1.3f из 20 ломтиков и 20 срезов. Я выбрал значение двадцать, потому что я не хотел, чтобы сфера была совершенно гладкой. При использовании меньшого количества кусков сфера будет грубой (не гладкой) и будет не совсем очевидно, что сфера вращается, когда сферическое наложение включено. Проиграйтесь с этими значениями. Важно обратить ваше внимание на то, что большая детализация требует больше процессорного времени.
case 1: // Эффект 1 - сфера
glRotatef(angle*1.3f, 1.0f, 0.0f, 0.0f); // Вращение по оси x
glRotatef(angle*1.1f, 0.0f, 1.0f, 0.0f); // Вращение по оси y
glRotatef(angle*1.2f, 0.0f, 0.0f, 1.0f); // Вращение по оси z
gluSphere(quadratic,1.3f,20,20); // Прорисовка сферы
break; //Конец прорисовки сферы
Сейчас мы нарисуем цилиндр. Мы начнём с простого вращения по x, y, z осям. Наш цилиндр будет иметь одинаковый верхний и нижний радиус равный 1 единице. Цилиндр будет иметь в высоту 3 единицы, и состоять из 32 ломтиков и 32 срезов. Если вы уменьшаете число кусков, цилиндр будет составлен из меньшого количества полигонов и будет казаться менее округленным.
Перед тем как рисовать цилиндр мы сдвинемся на –1.5 единиц по оси z. С помощью этого мы заставим наш цилиндр вращаться вокруг центра экрана. Общее правило к центрированию цилиндра: надо разделить на 2 его высоту и сдвинуться на полученный результат в отрицательном направлении по оси z. Если вы понятия не имеете о том, что я говорю, удалите строчку с translatef(…). Цилиндр будет двигаться вокруг своей оси, вместо центральной точки.
case 2: // Эффект 2 - цилиндр
glRotatef (angle*1.3f, 1.0f, 0.0f, 0.0f); // Вращение по оси x
glRotatef (angle*1.1f, 0.0f, 1.0f, 0.0f); // Вращение по оси y
glRotatef (angle*1.2f, 0.0f, 0.0f, 1.0f); // Вращение по оси z
glTranslatef(0.0f,0.0f,-1.5f); // Центр цилиндра
gluCylinder(quadratic,1.0f,1.0f,3.0f,32,32); // Прорисовка цилиндра
break; //Конец прорисовки цилиндра
}
Затем мы проверяем, является ли env TRUE. Если это так, то мы отключаем сферическое наложение. Мы вызываем glFlush() чтобы сбросить конвейер визуализации (чтобы быть уверенными, что прорисовка текущего кадра полностью завершена до начала прорисовки следующего кадра).
if (env) // Включено наложение окружения?
{
glDisable(GL_TEXTURE_GEN_S); // Вкл. автогенерация координат текстуры по S (Новое)
glDisable(GL_TEXTURE_GEN_T); // Вкл. автогенерация координат текстуры по T (Новое)
}
glFlush (); // Визуализация
}
Я надеюсь, что Вам понравился этот урок. Сейчас 2 часа ночи… Я работал над этим уроком последние шесть часов. Звучит безумно, но описать вещи так, чтобы это имело смысл, это нелегкая задача. Я прочитал урок три раза, и всё ещё пробую сделать его проще. Верите вы мне или нет, но для меня это очень важно, чтобы вы понимали, как работает код и почему он работает. Именно поэтому я нескончаемо повторяюсь, чрезмерно комментирую и т.д..
В любом случае. Мне бы хотелось услышать комментарии по поводу этого урока. Если вы найдете ошибки или вы хотели бы помочь мне сделать урок лучше, пожалуйста, войдите со мной в контакт, поскольку я сказал, что это моя первая попытка с AVI. Обычно я не пишу урок по теме, которую я только что изучил, но мое волнение извлекло всё самое лучшее из меня, плюс факт, что по этой теме очень мало информации обеспокоил меня. Я надеюсь, что я открою дверь потоку высококачественных демок с проигрыванием AVI и исходного кода. Может случиться… может нет. В любом случае вы можете использовать этот код тогда когда пожелаете нужным.
Огромное спасибо Фредстеру за его AVI файл с изображением лица. Лицо было одним из шести AVI, которые он послал мне для моего урока. Ни один мой вопрос не остался без ответа. Я посылал ему письма, и он выручил меня… Большое спасибо.
Большое спасибо, Джонатану Блоку. Если бы не он этого урока не существовало бы. Он заинтересовал меня AVI форматом высылая кусочки кода из его персонального AVI проигрывателя. Он также ответил мне на все вопросы относительно его кода. Важно то, что я ничего не заимствовал из его кода, его код был использован только для того чтобы понять, как проигрыватель работает. Мой проигрыватель открывает, декодирует и запускает AVI файлы, используя совершенно другой код!
Большое спасибо, каждому посетителю сайта. Без Вас этого сайта не было бы!
© Jeff Molofee (NeHe)
3 ноября 2003 (c) СhipSet