NeHe Tutorials Народный учебник по OpenGL
Урок 33. OpenGL

Loading Compressed And Uncompressed TGA's

 

Я видел много людей на канале #gamedev, форуме gamedev  и в других местах спрашивающих как загрузить изображение из TGA. В этом уроке объясняется процесс загрузки и декомпрессии TGA и сжатых методом RLE файлов. Этот пример переплетен с OpenGL, но я планирую сделать его более универсальным в будущем.

 

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

 

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

 

В начале файла добавьте следующие строки:

 

#ifndef __TEXTURE_H__// Определяем был ли заголовочный файл определен ранее

#define __TEXTURE_H__// Если нет, определяем его

 

Далее пролистайте файл до конца и добавьте:

 

#endif// Конец защиты от повторного включения__TEXTURE_H__

 

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

 

В этот заголовочный файл мы будем включать все стандартные заголовки, которые нам нужны в процессе работы. Добавьте следующие строки после команды #define __TGA_H__.

 

#pragma comment(lib, "OpenGL32.lib") // Линкуем Opengl32.lib

#include <windows.h> // Стандартный заголовочный файл Windows

#include <stdio.h> // Стандартный заголовочный файл для функций ввода/вывода

#include <gl\gl.h> // Стандартных заголовочный файл для OpenGL

 

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

 

Для хранения изображения и информации необходимой для генерирования текстур OpenGL мы будем использовать следующую структуру.

 

typedef struct

  {

    GLubyte* imageData; // Содержит всю информацию о цвете изображения

    GLuint  bpp;        // Содержит количество бит на пиксель     

    GLuint width;       // Ширина изображения 

    GLuint height;      // Высота изображения 

    GLuint texID;       // идентификатор текстуры для использования совместно с glBindTexture. 

    GLuint type;        // Информация хранимая в * ImageData (GL_RGB или GL_RGBA)

  } Texture;

 

Затем следует еще 2 структуры, используемые для обработки TGA файла.

 

typedef struct

  {

    GLubyte Header[12];   // Заголовок файла определяющий его тип

  } TGAHeader;

 

  typedef struct

  {

    GLubyte header[6];    // Содержит первые полезные 6 байт файла

    GLuint bytesPerPixel; // Количество байт на пиксель (3 или 4)

    GLuint imageSize;     // Размер памяти необходимый для хранения изображения

    GLuint type;          // Тип изображения, GL_RGB или GL_RGBA

    GLuint Height;        // Высота изображения         

    GLuint Width;         // Ширина изображения       

    GLuint Bpp;           // Количество бит на пиксель (24 или 32)

  } TGA;

 

Теперь мы должны объявить наши две структуры, чтобы иметь возможность их использовать в нашем коде.

 

  TGAHeader tgaheader; // Используется для хранения заголовка файла

  TGA tga;             // Используется для хранения информации о файле

 

Теперь мы должны определить пару заголовков файла в формате TGA, чтобы знать какие файлы являются корректными изображениями. Если это не сжатый TGA файл, то первый 12 байт будут “0 0 2 0 0 0 0 0 0 0 0 0” и “0 0 10 0 0 0 0 0 0 0 0 0” при RLE сжатии. Два этих значения позволят нам проверить, является ли файл корректным или нет.

 

  // Заголовок несжатого TGA

  GLubyte uTGAcompare[12] = {0,0, 2,0,0,0,0,0,0,0,0,0};

  // Заголовок сжатого TGA

  GLubyte cTGAcompare[12] = {0,0,10,0,0,0,0,0,0,0,0,0};

 

И, наконец, определяем пару функций используемых в процессе загрузки.

 

  // Загружает несжатый файл

  bool LoadUncompressedTGA(Texture *, char *, FILE *);

  // Загружает сжатый файл

  bool LoadCompressedTGA(Texture *, char *, FILE *);

 

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

 

Теперь мы должны подключит только что сделанный файл.

 

#include "tga.h"        // Подключаем сделанный файл

 

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

 

Далее мы займемся функцией называемой LoadTGA(...).

 

  // Загрузить файл TGA!

  bool LoadTGA(Texture * texture, char * filename)

 

Функция принимает 2 параметра. Первый, указатель на структуру с текстурой, которую вы должны объявить где-то в вашем коде (смотрите пример). Второй параметр указывает место нахождения файла.

 

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

 

  FILE * fTGA; // Объявляем указатель на файл

  fTGA = fopen(filename, "rb"); // Открываем файл на чтение

 

В следующих нескольких строках кода проводится проверка, был ли файл открыт корректно.

 

  if(fTGA == NULL)        // Если была ошибка

  {

    ...Error code...

    return false;         // Возвращаем False

  }

 

Далее мы пытаемся прочитать первые двенадцать байт файла в нашу структуру TGAHeader и проверить тип файла. Если чтение неудачно, закрываем файл, выводим сообщение об ошибке, и функция возвращает false.

 

  // Попытка прочитать заголовок файла

  if(fread(&tgaheader, sizeof(TGAHeader), 1, fTGA) == 0)

  {

    ...Здесь код ошибки...

    return false;        // При ошибки возвращаем false

  } 

 

Далее мы пытаемся определить какой это тип файла, сравнивая его с определенными нами заголовками файла TGA. Это даст нам знать, что этот файл сжатый, несжатый или неправильный. Для этого мы будем использовать функцию memcmp(…).

 

  // Если заголовок файла соответствует заголовку несжатого файла

  if(memcmp(uTGAcompare, &tgaheader, sizeof(tgaheader)) == 0)

  {

    // Загружаем несжатый TGA

    LoadUncompressedTGA(texture, filename, fTGA);

  }

  // Если заголовок файла соответствует заголовок сжатого файла

  else if(memcmp(cTGAcompare, &tgaheader, sizeof(tgaheader)) == 0)

  {                      

    // Загружаем сжатый TGA

    LoadCompressedTGA(texture, filename, fTGA);

  }

  else            // Если не соответствует никакому

  {

    ... Здесь код ошибки...           

    return false;        // Возвращаем false False

  } 

 

Мы начнем эту часть с загрузки несжатого файла. В основном это функция основана на Уроке 25.

 

Сначала, как обычно, мы начинаем с заголовка функции.

 

  // Загружаем несжатый TGA!

  bool LoadUncompressedTGA(Texture * texture, char * filename, FILE * fTGA)

  {

 

Эта функция получает три параметра. Первые два параметра такие же, как и в LoadTGA и передаются без изменения. Третий параметр, указатель на файл TGA, и поэтому позиция чтения в файле сохранится.

 

Далее мы пытаемся прочитать следующие 6 байт, и записать их в tga.header. Если ничего не выходит, мы устанавливаем код ошибки и возвращаем false.

 

  // Пытаемся прочитать следующие 6 байт

  if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)

  {                   

    ...Здесь код ошибки...

    return false;        // Возвращаем False

  }

 

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

 

  texture->width  = tga.header[1] * 256 + tga.header[0];  // Вычисляем высоту

  texture->height = tga.header[3] * 256 + tga.header[2];  // Вычисляем ширину

  texture->bpp = tga.header[4]; // Вычисляем количество бит на пиксель

  tga.Width = texture->width;   // Вычисляем высоту в локальной структуре

  tga.Height = texture->height; // Вычисляем ширину в локальной структуре

  tga.Bpp = texture->bpp;       // Вычисляем количество бит на пиксель в локальной структуре

 

 

Теперь мы проверяем, чтобы высота и ширина была хотя бы в один пиксель, и чтобы bpp было 24 или 32. Если какое-то значение находится вне пределов, то мы опять выводим сообщение об ошибке, закрываем файл и покидаем функцию.

 

// Убеждаемся что вся информация корректна

  if((texture->width <= 0) || (texture->height <= 0) || ((texture->bpp != 24) && (texture->bpp !=32)))

  {

    ...Здесь код ошибки...

    return false;        // Возвращаем False

  }

 

Здесь мы устанавливаем тип изображения: 24 бита – GL_RGB, 32 бита – GL_RGBA.


  if(texture->bpp == 24)        // Это изображение 24bpp?

    texture->type  = GL_RGB;    // Если да, устанавливаем GL_RGB

  else                          // если не 24, тогда 32

    texture->type  = GL_RGBA;   // устанавливаем GL_RGBA

 

Теперь мы высчитываем количество байт на пиксель и высчитываем полный размер изображения.

 

  tga.bytesPerPixel = (tga.Bpp / 8); // Высчитываем количество байт на пиксель

  // Считаем размер памяти необходимый для хранения изображения

  tga.imageSize = (tga.bytesPerPixel * tga.Width * tga.Height);

 

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

 

Затем, мы должны проверить, выделился ли нужный объем памяти. Если возникла ошибка, вызываем обработчик ошибки.

 

  // Выделяем память

  texture->imageData = (GLubyte *)malloc(tga.imageSize);

  if(texture->imageData == NULL)      // Убеждаемся что она была выделена

  {

    ...Здесь код ошибки...

    return false;        // Если нет, возвращаем False

  }

 

Здесь мы попытаемся считать изображение. Если не сможем, опять переключимся на код ошибки.

 

  // Пытаемся считать все изображение

  if(fread(texture->imageData, 1, tga.imageSize, fTGA) != tga.imageSize)

  {

    ...Здесь код ошибки...               

    return false;        // Если не получилось, возвращаем False

  }

 

TGA хранит цвета в порядке, обратно тому, который нужен OpenGL так, что мы должны изменить его с BGR на RGB. Для этого мы меняем местами первый и третий байт в каждом пикселе.

 

Стив Томас добавляет: Я получил небольшое ускорение в коде загрузки TGA. Это касается конвертирования с BGR в RGB, используя всего 3 бинарные операции. Вместо использования временной переменной вы делаете 3 раза XOR этих двух байт.

 

Затем мы закрываем файл и успешно выходим из функции.

 

  // Начинаем цикл

  for(GLuint cswap = 0; cswap < (int)tga.imageSize; cswap += tga.bytesPerPixel)

  {

    // Первый байт XOR третий байт XOR первый байт XOR третий байт

    texture->imageData[cswap] ^= texture->imageData[cswap+2] ^=

    texture->imageData[cswap] ^= texture->imageData[cswap+2];

  }

 

  fclose(fTGA);          // Закрываем файл

  return true;          // Возвращаем true

}

 

Это все относилось к загрузке несжатого TGA файла. Загрузка файла сжатого RLE немного сложнее. Как обычно мы читаем заголовок, чтобы узнать высоту, ширину и bpp, также как и в несжатом варианте, так что я буду использовать предыдущий код, и вы можете к нему, обратится для полного понимания.

 

bool LoadCompressedTGA(Texture * texture, char * filename, FILE * fTGA)

  {

    if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)

    {

      ...Здесь код ошибки...

    }             

    texture->width  = tga.header[1] * 256 + tga.header[0];

    texture->height = tga.header[3] * 256 + tga.header[2];

    texture->bpp  = tga.header[4];

    tga.Width  = texture->width;

    tga.Height  = texture->height;

    tga.Bpp  = texture->bpp;

    if((texture->width <= 0) || (texture->height <= 0) ||

       ((texture->bpp != 24) && (texture->bpp !=32)))

    {

      ... Здесь код ошибки...

    }                }

 

  if(texture->bpp == 24)        // Это изображение 24bpp?

    texture->type  = GL_RGB;    // Если да, устанавливаем GL_RGB

  else                          // если не 24, тогда 32

    texture->type  = GL_RGBA;   // устанавливаем GL_RGBA

 

    tga.bytesPerPixel  = (tga.Bpp / 8);

    tga.imageSize    = (tga.bytesPerPixel * tga.Width * tga.Height);

 

Теперь мы должны выделить память для хранения изображения ПОСЛЕ декомпрессии и будем использовать malloc. Если невозможно выделить память, выполняем код ошибки, и возвращаем false.

 

// Выделяем память для хранения изображения

  texture->imageData  = (GLubyte *)malloc(tga.imageSize);

  if(texture->imageData == NULL)      // Если невозможно выделить память...

  {

    ...Здесь код ошибки...

    return false;        // Возвращаем False

  }

 

Далее нам нужно определить из какого количества пикселей состоит изображение. Мы будем хранить это значение в переменной "pixelcount".

 

Также мы должны определить, на каком пикселе находимся, и в какой байт imageData сейчас выводим, чтобы предотвратить переполнение и перезапись предыдущих данных.

 

Мы выделим достаточно памяти для хранения одного пикселя.

 

  GLuint pixelcount = tga.Height * tga.Width;  // Количество пикселей в изображении

  GLuint currentpixel  = 0;     // Пиксель с который мы сейчас считываем

  GLuint currentbyte  = 0;      // Байт который мы зарисуем в Imagedata

  // Хранилище для одного пикселя

  GLubyte * colorbuffer = (GLubyte *)malloc(tga.bytesPerPixel);

 

Затем идет большой цикл.

 

Давайте разобьем его на несколько частей.

 

Сначала мы объявляем переменную для хранения идентификатора секции данных, он определяет, является следующая секция RLE или RAW и какого она размера. Если идентификатор меньше или равняется 127, тогда это RAW секция, и при этом значение идентификатора – это количество цветов минус один, которые надо считать и скопировать в память перед переходом к другому байту  идентификатора. Следовательно, мы прибавляем единицу к полученному значению и затем считываем все эти пиксели и копируем в ImageData, также как мы делали и с несжатыми данными. Если идентификатор больше 127, то это количество раз, которое непрерывно повторяется пиксель. Для получения количества повторений мы берем полученное значение и отнимаем от него 127. Затем мы считываем пиксель и непрерывно копируем его в память указанное количество раз.

 

Сначала мы читаем заголовок в один байт.

 

do  // Начало цикла

  {

  GLubyte chunkheader = 0; // Значение для хранения идентификатора секции

  // Пытаемся считать идентификатора секции

  if(fread(&chunkheader, sizeof(GLubyte), 1, fTGA) == 0)

  {

    ...Код ошибки...

    return false;        // Если неудача, возвращаем False

  }

 

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

 

  if(chunkheader < 128)   // Если секция является 'RAW' секцией

  {                         

    chunkheader++;        // Добавляем единицу для получения количества RAW пикселей

 

Затем запускаем еще один цикл для чтения информации о цветах. Он будет выполняться количество раз указанных в идентификаторе секции, и считывать один пиксель за раз.

 

Сначала мы считываем и проверяем информацию о пикселе. Информация об одном пикселе хранится в переменной colorbuffer. Затем мы проверяем, является ли это RAW идентификатором. Если да, прибавляем единицу и получаем количество пикселей следующих за ним.

 

  // Начало цикла чтения пикселей

  for(short counter = 0; counter < chunkheader; counter++)

  {

    // Пытаемся прочитать 1 пиксель

    if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) != tga.bytesPerPixel)

    {

      ...Код ошибки...

      return false;      // Если ошибка, возвращаем False

    }

 

Следующая часть нашего цикла будет брать значения цветов сохраненных в colorbuffer и записывать их в imageData для использования позже. В этом процессе пиксели будут зеркально отражены из BGR в RGB или из ABGR в RGBA в зависимости от количества бит на пиксель. После завершения мы инкрементируем текущий байт и текущего счетчика пикселей.

 

  texture->imageData[currentbyte] = colorbuffer[2];       // Записать байт 'R'

  texture->imageData[currentbyte + 1  ] = colorbuffer[1]; // Записать байт 'G'

  texture->imageData[currentbyte + 2  ] = colorbuffer[0]; // Записать байт 'B'

  if(tga.bytesPerPixel == 4)          // Если это 32bpp изображение...

  {

    texture->imageData[currentbyte + 3] = colorbuffer[3];  // Записать байт 'A'

  }

  // Увеличиваем счетчик байтов на значение равное количеству байт на пиксель

  currentbyte += tga.bytesPerPixel;

  currentpixel++;          // Увеличиваем количество пикселей на 1

 

Следующая часть работает с идентификатором представляющим RLE секцию. Первое что мы делаем, это вычитаем 127 из chunkheader для получения количества повторений следующего цвета.

 

  else  =// Если это RLE идентификатор

  {

    chunkheader -= 127; // Вычитаем 127 для получения количества повторений

 

Затем мы пытаемся считать значение следующего цвета.

 

  // Читаем следующий пиксель

  if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) != tga.bytesPerPixel)

  { 

    ...Код ошибки...

    return false;        // Если ошибка, возвращаем False

  }

 

Далее начинаем цикл для копирования только что прочитанного пикселя в память количество раз, указанных в RLE идентификаторе.

 

Затем мы копируем значение цветов в imageData, меняя местами R и B.

 

Затем мы инкрементируем текущий пиксель и байт, так что мы находимся в правой части, когда снова зарисуем значение.

 

  // Начинаем цикл

  for(short counter = 0; counter < chunkheader; counter++)

  {

    // Копируем байт 'R'

    texture->imageData[currentbyte] = colorbuffer[2];

    // Копируем байт 'G'

    texture->imageData[currentbyte + 1  ] = colorbuffer[1];

    // Копируем байт 'B'

    texture->imageData[currentbyte + 2  ] = colorbuffer[0];

    if(tga.bytesPerPixel == 4)    // Если это 32bpp изображение

    {

      // Копируем байт 'A'

      texture->imageData[currentbyte + 3] = colorbuffer[3];

    }

    currentbyte += tga.bytesPerPixel;  // Инкрементируем счетчик байтов

    currentpixel++;        // Инкрементируем счетчик пикселей

  }

 

Затем мы продолжаем основной цикл до тех пор, пока у нас справа есть пиксели для чтения.

 

Затем мы закрываем файл и возвращаем true.

 

    }

    while(currentpixel < pixelcount); // Еще есть пиксели для чтения? ... Начинаем цикл с начала

    fclose(fTGA);        // Закрываем файл

    return true;        // Возвращаем true

  }

 

Теперь у нас есть изображение готовое для glGenTextures и glBindTexture. Я предлагаю вам обратится к урокам №6 и №24 за более подробной информации по этим командам. Я не гарантирую что мой код без ошибок, но я приложил усилия на их устранение. Особые благодарности Джефу “NeHe” Молофи за его великолепные уроки и Тренту “ShiningKnight ” Полак за его помощь мне в корректировке этого урока. Спасибо.

 

Evan Pipho (Terminate)
Jeff Molofee (NeHe)

PMG  29 июля 2005 (c)  Александр Кириченко