Работа с расширениями OpenGL с использованием NVIDIA OpenGL SDK 5.1. (Часть 7) (2 стр)
Автор: Сергей Гайдуков
Сохранение сжатых текстур на диске
Хотя сжатые текстуры и занимают меньше места в видеопамяти, они храняться на диске в несжатом виде. С другой стороны, у разработчика может возникнуть желание хранить сжатые текстуры на диске в сжатом виде, что позволит увеличить скорость их загрузки, т.к. автокомпрессия большого объёма текстур может длиться несколько секунд.
Для того, чтобы сохранить сжатые текстуры на диске, программист должен выполнить следующие действия:
1. Выполнить команду glGetTexLevelParameteriv() с параметром GL_TEXTURE_COMPRESSED_ARB, чтобы убедится, что текстура хранится в сжатом виде.
2. Выполнить команду glGetTexLevelParameteriv() с параметром GL_TEXTURE_INTERNAL_FORMAT для определения формата сжатия текстуры. Эту величину надо будет сохранить, т.к. она понадобится при распаковке текстур.
Для каждого MipMap - уровня:
3. Определить и сохранить на диске размер текстуры при помощи команды glGetTexLevelParameteriv() с параметром GL_TEXTURE_IMAGE_SIZE_ARB.
4. При помощи команды glGetCompressedTexImageARB() получить образ сжатой текстуры и сохранить его на диске.
Загрузка текстуры из файла выполняется в точности наоборот:
1. Загрузка сохранённых параметров текстуры.
2. Загрузка каждого MipMap уровня сжатой текстуры в видеопамять с использованием команды glCompressedTexImage2DARB().
Для того, что бы сделать всё вышесказанное более понятным, я написал небольшую программу, которая умеет сохранять и считывать сжатые текстуры с диска (Ex03). В процессе создания этой программы я столкнулся со следующей проблемой - программа должна уметь запрашивать от пользователя имена файлов текстур. Но т.к. GLUT не располагает средствами для создания диалоговых окон, мне пришлось выбирать один из вариантов построения программы:
1. Создание OpenGL приложения при помощи библиотеки MFC или аналогичной. Но в этом случае наша программа уже не будет кроссплатформенной.
2. Создание графической части приложения при помощи GLUT, а интерфейсной части - с использованием Win32 API. Возможен ещё один вариант - создание интерфейсной части с использованием Borland Delphi или аналогичного RAD-средства и помещение её в библиотеку dll, которая в последствии подключалась бы к главной GLUT-программе. В этом случае для переноса на другую платформу надо будет переписать лишь интерфейсную часть программы.
3. Разработка собственного диалогового интерфейса для GLUT. Это, наверное, самый лучший вариант, но и самый трудный в реализации.
4. Организация интерфейса с пользователем при помощи командной строки. Относительно простая реализация, большая гибкость и расширяемость.
После долгих раздумий я решил остановиться на последнем варианте. Тем более, для реализации этого интерфейса достаточно лишь добавить поддержку командной строки в интерактор glut_console.
Для упрощения разработки кода консоли строка редактирования будет поддерживать лишь ввод символов и их удаление клавишей "Backspace". Наша консоль будет поддерживать лишь команды вида
Команда [параметр]
Кроме того, из-за того, что вывод символов на экран осуществляется с использованием функций GLUT, она не будет поддерживать русского языка.
Код поддержки собственно командной строки довольно тривиален: символы, вводимые с клавиатуры, накапливаются в поле cmd, которое затем, при нажатии клавиши "Enter", передаётся в качестве параметра методу processCmd. Поэтому, мы сразу перейдём к рассмотрению организации интерфейса между консолью и прикладной программой.
Сначала в программе определяются идентификаторы консольных команд, которые лежат в интервале 1..32767. Нулевой идентификатор означает "нет такой команды", а номера, начиная с 32768, используются для служебных команд.
После этого, программист должен опередить функцию - обработчик команд, которая обычно имеет примерно следующий вид:
void ConsoleCmd (GLenum Cmd, string Param) { switch ( Cmd) { //Если команда CM_LOAD (loadfile) case CMD_LOAD: //Если параметр равен "?" (команда help производит опрос команд), то выводим //краткую справку по команде if ( Param=="?") console.add( "loadfile: loads graphic file"); else //Если параметр равен "/?", то выводим полную справку по команде if ( ( Param=="/?") || Param.empty( )) { console.add( "Loads texture."); console.add( ""); console.add( "loadfile <filename>"); console.add( "where <filename> is JPG or SCI graphic file"); } else //Иначе - загружаем текстуру из файла Param { LoadTexture( Param); } break; case CMD_SAVE: ... }
В качестве параметров обработчику передаются идентификатор команды и её параметр. Существует два предопределённых значения параметра "?" и "/?", на которые команда обязана корректно реагировать. Значение "?" используется, когда интерактор glut_console консоли хочет получить краткую информацию о команде вида "название команды: что она делает", а "/?" - для получения подробной информации о команде. На остальные значения команда может реагировать по собственному усмотрению.
Сначала программа должна зарегистрировать обработчик команд, присвоив полю cmdProc адрес обработчика:
console.cmdProc=ConsoleCmd;
Затем зарегистрируются сами команды при помощи метода addCmd:
console.addCmd("loadtexture", CMD_LOAD); console.addCmd( "savetexture", CMD_SAVE); console.addCmd( "compress", CMD_COMPRESS); console.addCmd( "next", CMD_NEXT);
В качестве параметров он принимает название команды и её идентификатор, после чего он помещает их в ассоциативный массив cmds.
Как указывалось выше, первичная обработка команды происходит внутри метода processCmd(). Сначала он производит простой синтаксический разбор строки и выделяет из неё команду и параметр. Затем команда ищется в ассоциативном массиве cmds и, если она определена, передаётся пользовательскому обработчику команд. В противном случае выдаётся сообщение "unknown command". Кроме того, существуют две встроенные команды "exit" и "help". Команда "exit" предназначена для завершения работы программы, "help" - для получения краткой справки по всем командам. Чтобы сделать всё это более понятным, ниже приведён фрагмент кода метода processCmd(), отвечающий за обработку команд:
if (cmdProc) { switch ( cmds[command]) { case 0: add( "Unknown command"); break; case CMD_EXIT: if ( param=="?") add( "exit: quits the program"); else ::exit( 0); break; case CMD_HELP: if ( param=="?") add( "help: shows this information"); else for ( map<string, GLenum>::iterator itor=cmds.begin( ); itor!=cmds.end( ); itor++) { processCmd( itor->first+" ?"); } break; default: cmdProc( cmds[command], param); break; } } else add( "Internal Error: CmdProc callback function is not defined"); }
На рисунке 2 приведён внешний вид консоли нашего приложения после выполнения команды help:
Рисунок 2. Консоль после выполнения команды help.
На рисунке видно, что наша программа поддерживает 4 команды. Команда "loadtexture" загружает текстуру из файла, "savetexture" - сохраняет в файл с расширением sci (моё собственное "изобретение"), "compress" - сжимает текстуру, используя указанный формат, а "next" - циклически переключает режимы сжатия текстур (без сжатия - DXT1 - DXT3 - DXT5). Например, последовательность команд:
loadtexture eburg.jpg compress DXT3 next savetexture eburg.sci loadtexture eburg.sci
выполняет следующие действия:
1. загружает текстуру из файла eburg.jpg
2. сжимает её, используя формат сжатия DXT3
3. изменяет формат сжатия на следующий (DXT5)
4. сохраняет сжатую текстуру в файле eburg.sci
5. загружает сжатую текстуру из файла eburg.sci
Если вы попробуете выполнить эти команды, то получите неожиданный результат - в сжатом файле будет находиться перевёрнутая текстура (рисунок 3). Это связано с тем, что библиотека nv_util загружает файлы jpeg в перевернутом виде. Как вы помните, для исправления этой ошибки мы используем специальную матрицу текстуры (см. [gurl=http://opengl.gamedev.ru/articles/?id=113]6-ю часть[/url] статьи). Но этот приём исправляет только изображение на экране, сама же текстура остаётся перевёрнутой. В результате в сжатый файл так же записывается перевёрнутая текстура.
Единственный выход из данной ситуации - переворачивать текстуру, загружаемую из jpg-файла, по-настоящему. Для выполнения этой операции я написал небольшую функцию, которая проводит постобработку jpg-текстуры:
int MirrorTexture(tga::tgaImage* pTextureImage) { int components; //Определяем число цветовых составляющих switch ( pTextureImage->format) { case GL_RGB: components=3; break; case GL_RGBA: components=4; break; //Для поддержки других форматов вставьте сюда свои блоки case default: return false; } GLubyte* Image1=pTextureImage->pixels; for ( int j=0; j<pTextureImage->height/2; j++) { GLubyte* Image2 = pTextureImage->pixels + ( pTextureImage->width-j-1)*pTextureImage->width*components; for ( int i=0; i<pTextureImage->width*components; i++) { //Меняем местами симметричные тексели текстуры GLubyte tmp=*Image1; *Image1=*Image2; *Image2=tmp; Image1++; Image2++; } } return true; }
Рисунок 3. Перевёрнутая сжатая текстура формата DXT5
Теперь сжатая текстура сохраняется корректно (рисунок 4).
Рисунок 4. Сжатая текстура формата DXT5
Ниже приведен фрагмент кода, отвечающий за сохранение файлов сжатых текстур формата sci.