Желаю здравствовать всем труженикам «мыша» и клавиатуры. В этой статье мне хотелось бы поделиться опытом загрузки различных графических и не очень форматов данных. В первую очередь, рассказ пойдет для новичков, но, думаю, и многочисленные опытные гейм разработчики найдут в нем много интересного.
И так, о чем же пойдет речь? Начиная свой новый проект, мы решили, что хорошо бы спрятать все игровые данные от глаз любопытных пользователей. Да и с эстетической точки зрения, чем меньше файлов, тем нагляднее структура. Дабы не изобретать велосипед, в качестве ресурсного файла был взят обычный zip-архив. И все бы ничего, да вот для чтения текстур из файлов (*.jpg и *.png) используются сторонние библиотеки, с ними то и возникло большинство проблем. Вот о них и пойдет разговор.
1. Для чтения из ZIP-архива понадобиться библиотека unzip. Найти ее можно по этому адресу:
http://www.winimage.com/zLibDll/unzip.html. Дома у меня версия 1.00, за 10 сентября 2003 г.
2. Читать PNG формат будем при помощи библиотечки libpng. Домашняя страничка здесь:
http://www.libpng.org/pub/png/libpng.html. Я пользовался версией 1.2.5, от 3 октября 2002 г.
3. Картинки в JPEG-формате понимает libjpeg. Последние версии можно найти по этому адресу:
ftp://ftp.uu.net/graphics/jpeg/. Моя версия библиотеки значится под номером 6 бета, и датируется 27 марта 1998 г. Страница этой библиотеки здесь: http://www.ijg.org/.
4. Пример, поставляемый с этой статьей.
5. Ко всему этому комплекту желательно иметь какую-нибудь визуальную студию с компилятором «С++».
Стоит отметить, что все вышеперечисленные библиотеки и примеры работы с ними можно найти в «nVidia SDK».
Чтение ZIP-архивов
Наша игра общается с винчестером через класс-оболочку cFile, который умеет писать и читать как обычные файлы, так и файлы, запакованные в zip (только чтение). Описан он в файле «filemngr.h», а реализован соответственно в «filemngr.cpp». Для унификации работы, параметры, принимаемые методами этого класса, и возвращаемые значения совпадают с параметрами функций из заголовочного файла <stdio.h>. Дабы не отвлекаться по мелочам, в статье рассмотрим только те части кода, которые непосредственно необходимы для чтения ZIP-архивов.
Для работы с архивом, понадобится две переменных. Первая – указатель на ZIP-файл, подобная FILE* в стандартных потоках ввода/вывода. Вторая представляет собой структуру, содержащую информацию о файле в архиве.
// данные для сжатых ZIP'ом файлов// указатель на ZIP файл
unzFile m_pZFile;
// информация о зазипованном файле (интересен размер распакованного файла)
unz_file_info m_ZInfo;
Открытие файлов — метод Open.
// смотрим, откуда юзверь хочет достать файлif( szZipName ) { // из зипа// определяем тип файла (запись/чтение), сейчас доступно только чтениеswitch( szMode[0]) {
case'r': m_pData->m_Type = cFileData::e_ZipFile; break;
}
if( m_pData->m_Type == cFileData::e_Unknown )return( cFile::cError::WrongMode );
//-------------------------------------------------------------------// пытаемся открыть файл и распаковать егоtry {
// открываем zip файл
m_pData->m_pZFile = unzOpen(szZipName);
if( !m_pData->m_pZFile )throw cZipCatch("^7File Manager:^6 ZIP file %s ^4not found^6\n", cFile::cError::ZipNotFound);
// ищем в нем наш файлif( unzLocateFile(m_pData->m_pZFile, szFileName, 0) != UNZ_OK )throw cZipCatch("^7File Manager:^6 File %s ^4not found^6 in ZIP file\n",
cFile::cError::FileNotFound);
// если мы его нашли, то получаем о нем информациюif( unzGetCurrentFileInfo(m_pData->m_pZFile, &m_pData->m_ZInfo, 0,0, 0,0, 0,0) != UNZ_OK )throw cZipCatch("^7File Manager:^6 ^4Internal error^6 in ZIP file - couldn't get file info\n",
cFile::cError::DamageZip);
// открываем наш файл для чтенияif( unzOpenCurrentFile(m_pData->m_pZFile) != UNZ_OK )throw cZipCatch("^7File Manager:^6 ^4Internal error^6 in ZIP file - couldn't open file\n",
cFile::cError::DamageZip);
}
catch( cZipCatch err ) {
// сначала выведем ошибку в консоль// ...// удаляем промежуточные данные (те которые в блоке try)if( m_pData->m_pZFile ) unzClose( m_pData->m_pZFile );
// теперь удалим основные данные и вернем ошибку
SAFE_DELETE( m_pData );
return( err.m_iError );
}
}
Метод поиска и открытия файла в архиве, очень похож на стандартный сишный стиль. Сначала, как и с обычным I/O потоком открываем архив функцией unzOpen(). В случае отсутствия файла или некорректного архива она вернет 0, если же все прошло гладко, то результатом будет указатель, используемый в других zip-функциях. Следующий наш шаг — поиск необходимого файла в архиве при помощи функции unzLocateFile(), помимо этого она сделает его файлом по умолчанию. В качестве параметров ей передается само имя файла, а также метод сравнения имен, зависящий от регистра букв или нет. В случае успешного поиска результат функции будет равен UNZ_OK, в противном случае — UNZ_END_OF_LIST_OF_FILE. В процессе работы с файлом, часто приходится запрашивать его размер, к сожалению, ничего подобного fseek() моя старая версия библиотеки не поддерживала, поэтому, и по сей день, приходится брать информацию о файле непосредственно у zip-архива. Для этих целей служит функция unzGetCurrentFileInfo(). Помимо этого она позволяет узнать: версию zip’а, необходимого для распаковки, метод компрессии, дату последней модификации файла, размер файла в сжатом виде и пр. Так как пишем не архиватор, то об этих вещах, я рассказывать не буду. И последний шаг — это открытие текущего файла для чтения при помощи функции unzOpenCurrentFile(). В случае успеха, он вернет UNZ_OK.
Закрытие запакованного файла (метод Close()) происходит в 2 этапа: сначала мы закрываем файл внутри архива при помощи функции unzCloseCurrentFile(), а затем сам архив, используя unzClose().
Стоит заметить, что в моей версии библиотеки, функция unzClose(), не может сама закрывать читаемый документ, поэтому первый этап обязателен.
Получение данных из запакованного файла происходит при помощи метода Read():
// читаем из зип-файлаif( m_pData->m_Type == cFileData::e_ZipFile ) {
int err = unzReadCurrentFile(m_pData->m_pZFile, pBuffer, (unsignedint)(uiSize * uiCount));
if( err <0) {
err = 0; // была ошибка - вернем 0
}
return( err / uiSize ); // возвращаем как и fread кол-во полностью прочитанных элементов (а не байт)
}
Функция unzReadCurrentFile() заполняет буфер pBuffer, массивом байт из уже распакованного файла, последний параметр указывает на размер буфера. В случае успеха, функция возвращает: число байт занесенное в буфер, 0 если достигнут конец файла и отрицательное число если произошла какая-то ошибка. Так как метод Read() я старался сделать похожим на стандартную функцию fread(), то возвращается не кол-во прочитанных байт, а кол-во «полных» элементов, в связи с этим и происходит деление на размер элемента.
Чтение PNG-формата
Теперь, когда разобрались с чтением запакованных файлов, можно приступить к главному — чтению графических форматов. Благо все библиотеки предоставляют возможность работы с пользовательскими I/O потоками. Для этого, библиотеке нужно описать функцию чтения данных из файла:
На вход подается структура, содержащая информацию о библиотеке, массив байт, в который нужно занести прочитанную информацию, и длина этого массива. Первой строкой определяется указатель на поток данных. Как настроить на него PNG-библиотеку будет сказано ниже. Вторая строка — побайтовое чтение из файла в массив. На этом написание вспомогательных функций закончено, можно приступать к чтению файла.
Первым делом, откроем файл и считаем из него сигнатуру, которая позволит узнать тип файла. Обычно для PNG-формата считывают первые восемь байт:
// проверяем сигнатуру файла (первые number байт)
png_byte sig[number] = {0};
file.Read(sig, sizeof(png_byte), number);
if( !png_check_sig(sig, number)) {
file.Close();
return( cDataManager::cError::UnknownFormat );
}
// проверка прошла успешно - это png-файл
Тестируется сигнатура при помощи функции png_check_sig(), на вход которой поступает массив из считанных байт и его длина. На выходе в случае несовпадения сигнатур, будет 0, в противном случае можно считать, что файл правильный. Следующий шаг — создание структур, содержащих информацию о библиотеке и файле:
// создаем внутреннюю структуру png для работы с файлом// последние параметры - структура, для функции обработки ошибок и варнинга (последн. 2 параметра)
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, 0, 0);
// создаем структуру с информацией о файле
png_infop info_ptr = png_create_info_struct(png_ptr);
Затем говорим библиотеке, что мы уже считали восемь байт и записываем информацию о файле в структуру, созданную ранее. Нас будет интересовать информация о размерах картинки, глубине цвета и формате данных.
// говорим библиотеке, что мы уже прочли number байт, когда проверяли сигнатуру
png_set_sig_bytes(png_ptr, number);
// читаем всю информацию о файле
png_read_info(png_ptr, info_ptr);
// Эта функция возвращает инфу из info_ptr
png_uint_32 width = 0, height = 0; // размер картинки в пикселяхint bit_depth = 0; // глубина цвета (одного из каналов, может быть 1, 2, 4, 8, 16)int color_type = 0; // описывает какие каналы присутствуют:// PNG_COLOR_TYPE_GRAY, PNG_COLOR_TYPE_GRAY_ALPHA, PNG_COLOR_TYPE_PALETTE,// PNG_COLOR_TYPE_RGB, PNG_COLOR_TYPE_RGB_ALPHA...// последние 3 параметра могут быть нулями и обозначают: тип фильтра, тип компрессии и тип смещения
png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, 0, 0, 0);
Получив необходимые данные, можно проверить, подходят они нам или нет. В случае неудовлетворительного ответа, библиотеке можно сказать, чтобы она на этапе считывания массива данных, конвертировала их в удобный для нас формат:
// png формат может содержать 16 бит на канал, но нам нужно только 8, поэтому сужаем каналif(bit_depth == 16) png_set_strip_16(png_ptr);
// преобразуем файл если он содержит палитру в нормальный RGBif(color_type == PNG_COLOR_TYPE_PALETTE && bit_depth <= 8) png_set_palette_to_rgb(png_ptr);
// если в грэйскейле меньше бит на канал чем 8, то конвертим к нормальному 8-битномуif(color_type == PNG_COLOR_TYPE_GRAY && bit_depth <8) png_set_gray_1_2_4_to_8(png_ptr);
// и добавляем полный альфа-каналif(png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png_ptr);
В нашей игре допустимы картинки, содержащие информацию только об оттенках серого цвета (grayscale картинки). Если же, необходимо преобразование к RGB формату, то это можно сделать таким образом:
Теперь осталось только настроить гамму и можно будет приступить к чтению данных. Гамма складывается из двух компонент, так называемой гаммы монитора (screen gamma) и гаммы картинки. В документации к библиотеке указано, какое значение гаммы лучше подбирать для различных типов платформ и мониторов: 2.2 подходит для PC мониторов в освещенной комнате, 2.0 – в комнате с тусклым освещением и от 1.7 до 1.0 – для MacOS платформ. Гамма картинки может быть указана в самом файле, в случае если эта информация отсутствует, желательно присвоить стандартное значение гаммы, равное 0.45455.
double gamma = 0.0f;
// если есть информация о гамме в файле, то устанавливаем на 2.2if( png_get_gAMA(png_ptr, info_ptr, &gamma)) png_set_gamma(png_ptr, 2.2, gamma);
// иначе ставим дефолтную гамму для файла в 0.45455 (good guess for GIF images on PCs)else png_set_gamma(png_ptr, 2.2, 0.45455);
Теперь обновим информацию в библиотеке и заново перечитаем данные о картинке — они должны совпасть с теми, которые мы запросили:
// после всех трансформаций, апдейтим информацию в библиотеке
png_read_update_info(png_ptr, info_ptr);
// опять получаем все размеры и параметры обновленной картинки
png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, 0, 0, 0);
Предварительные приготовления закончены, можно приступать к чтению картинки. Оно будет происходить построчно, от нижней строки к верхней.
// определяем кол-во байт нужных для того чтобы вместить строку
png_uint_32 row_bytes = png_get_rowbytes(png_ptr, info_ptr);
// теперь, мы можем выделить память чтобы вместить картинку
png_byte* data = new png_byte[row_bytes * height];
// выделяем память, для указателей на каждую строку
png_byte **row_pointers = new png_byte * [height];
// сопоставляем массив указателей на строчки, с выделенными в памяти (res)// т.к. изображение перевернутое, то указатели идут снизу вверхfor(unsignedint i = 0; i < height; i++)
row_pointers[height - i - 1] = data + i * row_bytes;
// все, читаем картинку
png_read_image(png_ptr, row_pointers);
В приведенном выше коде определяется размер строки в байтах. Затем выделяется память для массива пикселей (размер строки, помноженный на количество строк). Чтобы после получения информации не заниматься перестановкой строк, просто выделяем еще один массив, в который занесем указатели на строки в обратном порядке, их и будем подсовывать библиотечной функции чтения всей картинки за раз png_read_image. Теперь картинка в памяти и остается только освободить память от указателей, закрыть библиотеку и файл:
// освобождаем память от указателей на строкиdelete[] row_pointers;
// освобождаем память выделенную для библиотеки libpng
png_destroy_read_struct(&png_ptr, 0, 0);
// закрываем файл
file.Close();
На этом рассказ о PNG-формате можно считать завершенным.