Урок 24. Лексемы, Расширения, Вырезка и Загрузка TGA
Этот урок далеко не идеален, но Вы определенно узнаете кое-что новое. Я получил довольно много вопросов о расширениях и о том, как определить какие расширения поддерживаются конкретным типом видеокарты. Этот урок научит Вас определять, какие OpenGL расширения поддерживаются любой из 3D видео карт.
Также я научу Вас прокручивать часть экрана, не влияя при этом на остальную, используя вырезку. Вы также научитесь рисовать ломаные линии (GL_LINE_STRIP - прим. пер.), и, что самое важное, в этом уроке мы не будем использовать ни библиотеку AUX ни растровые изображения. Я покажу Вам, как использовать TGA-изображения в качестве текстур. С TGA изображениями не только просто работать, они поддерживают ALPHA-канал, который позволит Вам в будущих проектах использовать некоторые довольно интересные эффекты. Первое, что Вы должны отметить в коде ниже - нет больше включения заголовочного файла библиотеки glaux (glaux.h). Важно отметить, что файл glaux.lib также можно не включать в проект. Мы не работаем с растровыми изображениями, так что нет смысла включать эти файлы в наш проект.
Используя glaux, я всегда получал от компилятора одно предупреждение (warning). Без glaux не будет ни предупреждений, ни сообщений об ошибках.
#include <windows.h> // Заголовочный файл для Windows
#include <stdio.h> // Заголовочный файл для стандартного ввода/вывода
#include <stdarg.h> // Заголовочный файл для переменного числа параметров
#include <string.h> // Заголовочный файл для работы с типом String
#include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32
#include <gl\glu.h> // Заголовочный файл для библиотеки GLu32
HDC hDC=NULL; // Частный контекст устройства
HGLRC hRC=NULL; // Постоянный контекст рендеринга
HWND hWnd=NULL; // Содержит дескриптор окна
HINSTANCE hInstance; // Содержит экземпляр приложения
bool keys[256]; // Массив для работы с клавиатурой
bool active=TRUE; // Флаг активности приложения
bool fullscreen=TRUE; // Флаг полноэкранного режима
Первое, что мы должны сделать - добавить несколько переменных. Переменная scroll будет использоваться для прокрутки части экрана вверх и вниз. Переменная maxtokens будет хранить количество лексем (расширений), поддерживаемых данной видеокартой. base хранит базу списков отображения для шрифта. swidth и sheight будут использоваться для захвата текущих размеров окна. Мы используем эти две переменные позднее в коде для облегчения расчета координат вырезки.
int scroll; // Используется для прокручивания экрана
int maxtokens; // Количество поддерживаемых расширений
int swidth; // Ширина вырезки
int sheight; // Высота вырезки
Gluint base; // База списков отображения для шрифта
Создадим структуру для хранения информации о TGA изображении, которое мы загрузим. Первая переменная imageData будет содержать указатель на данные, создающие изображение. bpp содержит количество битов на пиксель (количество битов, необходимых для описания одного пикселя - прим. пер.), используемое в TGA файле (это значение может быть 24 или 32 в зависимости от того, используется ли альфа-канал). Третья переменная width будет хранить ширину TGA изображения. height хранит высоту изображения, и texID будет указывать на текстуру, как только она будет построена. Структуру назовем TextureImage.
В строке, следующей за объявлением структуры, резервируется память для хранения одной текстуры, которую мы будем использовать в нашей программе.
typedef struct // Создать структуру
{
Glubyte *imageData; // Данные изображения (до 32 бит)
Gluint bpp; // Глубина цвета в битах на пиксель
Gluint width; // Ширина изображения
Gluint height; // Высота изображения
Gluint texID; // texID используется для выбора
// текстуры
} TextureImage; // Имя структуры
TextureImage textures[1]; // Память для хранения
// одной текстуры
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление WndProc
Теперь позабавимся! Эта часть кода будет загружать TGA файл, и конвертировать его в текстуру, которая будет использоваться в нашей программе. Следует отметить, что этот код загружает только 24 или 32 битные несжатые TGA файлы. Я хорошо постарался, для того чтобы это код мог работать как с 24, так и с 32 битными файлами. :) Я никогда не говорил, что я гениален. Я хочу отметить, что этот код не весь был написан мною самостоятельно. Много хороших идей я извлек из чтения различных сайтов в сети. Я только собрал хорошие идеи и скомбинировал их в код, который хорошо работает с OpenGL. Ни легко, ни очень сложно!
Мы передаем два параметра в функцию ниже. Первый параметр (*texture) указывает на место в памяти, где можно сохранить текстуру. Второй параметр (*filename) - имя файла, который мы хотим загрузить.
Массив TGAheader[] содержит 12 байт. Эти байты мы будем сравнивать с первыми 12 байтами загружаемого TGA файла, для того чтобы убедиться в том, что это действительно TGA файл, а не файл изображения другого типа.
В TGAcompare[] будут помещены первые 12 байт загружаемого TGA файла. После этого произойдет сравнение байтов TGAcompare[] с байтами TGAheader[], для того чтобы убедиться в полном их соответствии.
header[] будет хранить 6 первых ВАЖНЫХ байт заголовка файла (ширину, высоту и количество битов на пиксель). Переменная bytesPerPixel будет содержать результат деления количества битов на пиксель на 8, который будет являться уже количеством байтов на пиксель.
imageSize будет хранить количество байтов, которое требуется для создания изображения (ширина * высота * количество байтов на пиксель).
temp - временная переменная, будет использована для обмена байтов дальше в программе.
Последнюю переменную type я использую для выбора подходящих параметров построения текстуры, в зависимости от того, является TGA 24 или 32 битным. Если текстура 24-х битная, то мы должны использовать режим GL_RGB при построении текстуры. Если же текстура 32-х битная, то мы должны будем добавить alpha компоненту, т.е. использовать GL_RGBA (по умолчанию я предполагаю, что изображение 32-х битное, вот почему переменная type установлена в GL_RGBA).
bool LoadTGA(TextureImage *texture, char *filename)
// Загружаем TGA файл в память
{
Glubyte TGAheader[12]={0,0,2,0,0,0,0,0,0,0,0,0}; // Заголовок несжатого TGA файла
Glubyte TGAcompare[12]; // Используется для сравнения заголовка TGA файла
Glubyte header[6]; // Первые 6 полезных байт заголовка
Gluint bytesPerPixel; // Количество байтов на пиксель используемое в TGA файле
Gluint imageSize; // Количество байтов, необходимое для хранения изображения в памяти
Gluint temp; // Временная переменная
Gluint type=GL_RGBA; // Установим по умолчанию режим RBGA (32 BPP)
В первой строке кода ниже TGA файл открывается на чтение. file - указатель, который мы будем использовать для ссылки на данные в пределах файла. Команда fopen(filename, "rb") открывает файл filename, а параметр "rb" говорит нашей программе, что открыть файл нужно на чтение ([r]eading) как двоичный ([b]inary).
Инструкция if производит несколько действий. Во-первых, проверяется, не пуст ли файл. Если он пуст, то будет возвращено значение NULL, файл будет закрыт командой fclose(file) и функция вернет false.
Если файл не пуст, то мы попытаемся прочитать его первые 12 байтов в TGAcompare. Это сделает код в следующей строке: функция fread прочитает sizeof(TGAcompare) (12 байтов) из файла в TGAcompare. Затем мы проверим: соответствует ли количество байтов, прочитанных из файла, размеру TGAcompare, который должен быть равен 12 байтам. Если мы не смогли прочитать 12 байтов в TGAcompare, то файл будет закрыт, и функция возвратит false.
Если все прошло удачно, то мы сравниваем 12 байтов, которые мы прочитали в TGAcompare, с 12 байтами, которые хранятся в TGAheader.
Наконец, если все прошло великолепно, мы попытаемся прочитать следующие 6 байтов в header (важные байты). Если эти 6 байтов недоступны, файл будет закрыт и функция вернет false.
FILE *file = fopen(filename, "rb"); // Открытие TGA файла
if(file==NULL || // Существует ли файл
fread(TGAcompare,1,sizeof(TGAcompare),file)!=sizeof(TGAcompare) ||
// Если прочитаны 12 байтов заголовка
memcmp(TGAheader,TGAcompare,sizeof(TGAheader))!=0 || // Если заголовок правильный
fread(header,1,sizeof(header),file)!=sizeof(header)) // Если прочитаны следующие 6 байтов
{
if (file == NULL) // Если ошибка
return false; // Возвращаем false
else
{
fclose(file); // Если ошибка, закрываем файл
return false; // Возвращаем false
}
}
Если все прошло успешно, то теперь у нас достаточно информации для определения некоторых важных переменных. Первой переменной, которую мы определим, будет width. Мы хотим, чтобы width равнялась ширине TGA изображения. Эту ширину мы найдем, умножив значение, сохраненное в header[1], на 256. Затем мы добавим младший байт, сохраненный в header[0].
height вычисляется таким же путем, но вместо значений сохраненных в header[0] и header[1], мы используем значения, сохраненные в header[2] и header[3].
После того как мы вычислили ширину и высоту, мы должны проверить, что ширина и высота больше 0. Если ширина или высота меньше или равна нулю, файл будет закрыт и функция вернет false.
Также мы должны удостовериться, что TGA файл является 24 или 32 битным изображением. Это мы сделаем, проверив значение, сохраненное в header[4]. Если оно не равно ни 24, ни 32 (бит), то файл будет закрыт и функция вернет false.
В случае если бы Вы не осуществили проверку, возврат функцией значения false привел бы к аварийному завершению программы с сообщением "Initialization Failed". Убедитесь, что ваш TGA файл является несжатым 24 или 32 битным изображением!
// Определяем ширину TGA (старший байт*256+младший байт)
texture->width = header[1] * 256 + header[0];
// Определяем высоту TGA (старший байт*256+младший байт)
texture->height = header[3] * 256 + header[2];
if(texture->width <=0 || // Если ширина меньше или равна нулю
texture->height <=0 || // Если высота меньше или равна нулю
(header[4]!=24 && header[4]!=32)) // Является ли TGA 24 или 32 битным?
{
fclose(file); // Если где-то ошибка, закрываем файл
return false; // Возвращаем false
}
Теперь, когда мы вычислили ширину и высоту изображения, нам необходимо вычислить количество битов на пиксель, байтов на пиксель и размер изображения (в памяти).
Значение, хранящееся в header[4] - это количество битов на пиксель. Поэтому установим bpp равным header[4].
Если Вам известно что-нибудь о битах и байтах, то Вы должны знать, что 8 битов составляют байт. Для того чтобы вычислить количество байтов на пиксель, используемое в TGA файле, все, что мы должны сделать - разделить количество битов на пиксель на 8. Если изображение 32-х битное, то bytesPerPixel будет равняться 4. Если изображение 24-х битное, то bytesPerPixel будет равняться 3.
Для вычисления размера изображения мы умножим width * height * bytesPerPixel. Результат сохраним в imageSize. Так, если изображение было 100х100х32, то его размер будет 100 * 100 * 32/8 = 10000 * 4 = 40000 байтов.
texture->bpp = header[4]; // Получаем TGA бит/пиксель (24 or 32)
bytesPerPixel = texture->bpp/8; // Делим на 8 для получения байт/пиксель
// Подсчитываем размер памяти для данных TGA
imageSize = texture->width*texture->height*bytesPerPixel;
Теперь, когда нам известен размер изображения в байтах, мы должны выделить память под него. Это мы сделаем в первой строке кода ниже. imageData будет указывать на область памяти достаточно большого размера, для того, чтобы поместить туда наше изображение. malloc(imagesize) выделит память (подготовит память для использования), основываясь на необходимом размере (imageSize). Конструкция "if" осуществляет несколько действий. Первое - проверка того, что память выделена правильно. Если это не так, imageData будет равняться NULL, файл будет закрыт и функция вернет false.
Если память была выделена, мы попытаемся прочитать изображение из файла в память. Это осуществляет строка fread(texture->imageData, 1, imageSize, file). fread читает файл. imageData указывает на область памяти, куда мы хотим поместить прочитанные данные. 1 - это количество байтов, которое мы хотим прочитать (мы хотим читать по одному байту за раз). imageSize - общее количество байтов, которое мы хотим прочитать. Поскольку imageSize равняется общему размеру памяти, достаточному для сохранения изображения, то изображение будет прочитано полностью.
После чтения данных, мы должны проверить, что количество прочитанных данных совпадает со значением, хранящимся в imageSize. Если это не так, значит где-то произошла ошибка. Если были загружены какие-то данные, мы уничтожим их (освободим память, которую мы выделили). Файл будет закрыт и функция вернет false.
texture->imageData=(GLubyte *)malloc(imageSize); // Резервируем память для хранения данных TGA
if(texture->imageData==NULL || // Удалось ли выделить память?
fread(texture->imageData, 1, imageSize, file)!=imageSize)
// Размер выделенной памяти равен imageSize?
{
if(texture->imageData!=NULL) // Были ли загружены данные?
free(texture->imageData); // Если да, то освобождаем память
fclose(file); // Закрываем файл
return false; // Возвращаем false
} Если данные были загружены правильно, то дела идут хорошо :). Все что мы должны теперь сделать - это обменять местами Красные (Red) и Синие (Blue) байты. Данные в TGA файле хранятся в виде BGR (blue, green, red). Если мы не обменяем красные байты с синими, то все, что в нашем изображении должно быть красным, станет синим и наоборот.
Во-первых, мы создадим цикл (по i) от 0 до imageSize. Благодаря этому, мы пройдемся по всем данным. Переменная цикла (i) на каждом шаге будет увеличиваться на 3 (0, 3, 6, 9, и т.д.) если TGA файл 24-х битный, и на 4 (0, 4, 8, 12, и т.д.) - если изображение 32-х битное. Дело в том, что значение i всегда должно указывать на первый байт ([b]lue байт) нашей группы, состоящей из 3-х или 4-х байтов (BGR или BGRA - прим. пер.).
Внутри цикла мы сохраняем [b]lue байт в переменной temp. Затем мы берем [r]ed байт, хранящийся в texture->imageData[i+2] (помните, что TGA хранит цвета как BGR[A]. B - i+0, G - i+1 и R - i+2) и помещаем его туда, где находился [b]lue байт.
Наконец, мы помещаем [b]lue байт, который мы сохранили в переменной temp, туда, где находился [r]ed байт.
Если все прошло успешно, то теперь TGA хранится в памяти, как пригодные данные для OpenGL текстуры.
for(GLuint i=0; i<int(imageSize); i+=bytesPerPixel) // Цикл по данным, описывающим изображение
{ // Обмена 1го и 3го байтов ('R'ed и 'B'lue)
temp=texture->imageData[i]; // Временно сохраняем значение imageData[i]
texture->imageData[i] = texture->imageData[i + 2]; // Устанавливаем 1й байт в значение 3го байта
texture->imageData[i + 2] = temp; // Устанавливаем 3й байт в значение,
// хранящееся в temp (значение 1го байта)
}
fclose (file); // Закрываем файл
Теперь, когда у нас есть данные, пришло время сделать из них текстуру. Начнем с того, что сообщим OpenGL о нашем желании создать текстуру в памяти по адресу &texture[0].texID.
Очень важно, чтобы Вы поняли несколько вещей прежде чем мы двинемся дальше. В коде функции InitGL(), когда мы вызываем функцию LoadTGA() мы передаем ей два параметра. Первый параметр - это &textures[0]. В LoadTGA() мы не обращаемся к &textures[0], мы обращаемся к &texture[0](отсутсвует 's' в конце). Когда мы изменяем &texture[0], на самом деле изменяется &textures[0]. texture[0] отождествляется с textures[0]. Я надеюсь, что это понятно.
Таким образом, если мы хотим создать вторую текстуру, то мы должны передать в какестве параметра &textures[1]. В LoadTGA(), изменяя texture[0] мы будем изменять textures[1]. Если мы передадим &textures[2], texture[0] будет связан с &textures[2], и т.д.
Трудно объяснить, легко понять. Конечно, я не успокоюсь, пока не объясню это по-настоящему просто :). Вот бытовой пример. Допустим, что у меня есть коробка. Я назвал ее коробкой № 10. Я дал ее своему другу и попросил его заполнить ее. Моего друга мало заботит, какой у нее номер. Для него это просто коробка. Итак, он заполнил, как он считает "просто коробку" и возвратил мне ее. При этом для меня он заполнил коробку № 10. Он считает, что он заполнил обычную коробку. Если я дам ему другую коробку, названную коробкой № 11, и скажу «эй, можешь ли ты заполнить эту». Для него это опять всего лишь "коробка". Он заполнит и вернет мне ее полной. При этом для меня он заполнил коробку № 11. Когда я передаю функции LoadTGA() параметр &textures[1], она воспринимает его как &texture[0]. Она заполняет его текстурой, и после завершения ее работы, у меня будет рабочая текстура textures[1]. Если я передам LoadTGA() &textures[2], она опять воспримет его как &texture[0]. Она заполнит его данными, И я останусь с рабочей текстурой textures[2]. В этом есть смысл :).
Во всяком случае… в коде! Мы говорим LoadTGA() построить нашу текстуру. Мы привязываем текстуру и говорим OpenGL, что она должна иметь линейный фильтр.
// Строим текстуру из данных
glGenTextures(1, &texture[0].texID); // Сгенерировать OpenGL текстуру IDs
glBindTexture(GL_TEXTURE_2D, texture[0].texID); // Привязать нашу текстуру
// Линейная фильтрация
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
// Линейная фильтрация
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
Теперь посмотрим, был ли TGA файл 24-х или 32-х битным. Если он был 24-х битным, то установим type в GL_RGB (отсутствует альфа-канал). Если бы мы этого не сделали, то OpenGL попытался бы построить текстуру с альфа-каналом. Так как информация об альфа отсутствует, то или произошел бы сбой программы или появилось бы сообщение об ошибке.
if (texture[0].bpp==24) // Если TGA 24 битный
{
type=GL_RGB; // Установим 'type' в GL_RGB
}
Теперь мы построим нашу текстуру, таким же путем как делали это всегда. Но вместо того, чтобы просто воспользоваться типом (GL_RGB или GL_RGBA), мы заменим его переменной type. Таким образом, если программа определит, что TGA был 24-х битным, то type будет GL_RGB. Если же TGA был 32-х битным, то type будет GL_RGBA.
После того как текстура будет построена, мы возвратим true. Это даст знать коду InitGL(), что все прошло успешно.
glTexImage2D(GL_TEXTURE_2D, 0, type, texture[0].width, texture[0].height,
0, type, GL_UNSIGNED_BYTE, texture[0].imageData);
return true; // Построение текстуры прошло Ok, возвратим true
}
Код ниже является нашим стандартом построения шрифта из текстуры. Все Вы встречали этот код и раньше, если Вы прошли все уроки до этого. Здесь нет ничего нового, но я считаю, что должен включить этот код, для того, чтобы облегчить понимание программы.
Единственным отличием является то, что я привязываю текстуру textures[0].texID, которая указывает на текстуру шрифта. Я добавил только лишь .texID.
GLvoid BuildFont(Glvoid) // Построение нашего шрифта
{
base=glGenLists(256); // Создадим 256 списков отображения
// Выбираем нашу текстуру шрифта
glBindTexture(GL_TEXTURE_2D, textures[0].texID);
for (int loop1=0; loop1<256; loop1++) // Цикл по всем 256 спискам
{
float cx=float(loop1%16)/16.0f; // X позиция текущего символа
float cy=float(loop1/16)/16.0f; // Y позиция текущего символа
glNewList(base+loop1,GL_COMPILE); // Начало построение списка
glBegin(GL_QUADS); // Используем квадрат для каждого символа
glTexCoord2f(cx,1.0f-cy-0.0625f); // Коорд. текстуры (Низ Лево)
glVertex2d(0,16); // Коорд. вершины (Низ Лево)
glTexCoord2f(cx+0.0625f,1.0f-cy-0.0625f); // Коорд. текстуры (Низ Право)
glVertex2i(16,16); // Коорд. вершины (Низ Право)
glTexCoord2f(cx+0.0625f,1.0f-cy-0.001f); // Коорд. текстуры (Верх Право)
glVertex2i(16,0); // Коорд. вершины (Верх Право)
glTexCoord2f(cx,1.0f-cy-0.001f); // Коорд. текстуры (Верх Лево)
glVertex2i(0,0); // Коорд. вершины (Верх Лево)
glEnd(); // Конец построения квадрата (символа)
glTranslated(14,0,0); // Смещаемся в право от символа
glEndList(); // Конец построения списка
} // Цикл пока не будут построены все 256 списков
}
Функция KillFont не изменилась. Мы создали 256 списков отображения, поэтому мы должны будем уничтожить их, когда программа будет завершаться.
GLvoid KillFont(GLvoid) // Удаляем шрифт из памяти
{
glDeleteLists(base,256); // Удаляем все 256 списков
}
Код glPrint() немного изменился. Все буквы теперь растянуты по оси y, что делает их очень высокими. Остальную часть кода я объяснял в прошлых уроках. Растяжение выполняется командой glScalef(x,y,z). На оси x мы оставляем коэффициент равным 1.0, удваиваем размер (2.0) по оси y, и оставляем 1.0 по оси z.
GLvoid glPrint(GLint x, GLint y, int set, const char *fmt, …) // Здесь происходит печать
{
char text[1024]; // Содержит нашу строку
va_list ap; // Указатель на список аргументов
if (fmt == NULL) // Если текста нет
return; // Ничего не делаем
va_start(ap, fmt); // Разбор строки переменных
vsprintf(text, fmt, ap); // И конвертирование символов в реальные коды
va_end(ap); // Результат помещаем в строку
if (set>1) // Если выбран неправильный набор символов
{
set=1; // Если да, выбираем набор 1 (Italic)
}
glEnable(GL_TEXTURE_2D); // Разрешаем двумерное текстурирование
glLoadIdentity(); // Сбрасываем матрицу просмотра модели
glTranslated(x,y,0); // Позиционируем текст (0,0 - Верх Лево)
glListBase(base-32+(128*set));// Выбираем набор шрифта (0 или 1)
glScalef(1.0f,2.0f,1.0f); // Делаем текст в 2 раза выше
glCallLists(strlen(text),GL_UNSIGNED_BYTE, text);// Выводим текст на экран
glDisable(GL_TEXTURE_2D); // Запрещаем двумерное текстурирование
}
ReSizeGLScene() устанавливает ортографическую проекцию. Ничего нового. 0,1 - это верхний левый угол экрана. 639, 480 соответствует нижнему правому углу экрана. Это дает нам точные экранные координаты с разрешением 640х480. Заметьте, что мы устанавливаем значение swidth равным текущей ширине окна, а значение sheight равным текущей высоте окна. Всякий раз, при изменении размеров или перемещении окна, переменные sheight и swidth будут обновлены.
GLvoid ReSizeGLScene(GLsizei width, GLsizei height) // Изменение размеров и инициализация GL окна
{
swidth=width; // Устанавливаем ширину вырезки в ширину окна
sheight=height; // Устанавливаем высоту вырезки в высоту окна
if (height==0) // Предотвращаем деление на нуль
{
height=1; // Делаем высоту равной 1
}
glViewport(0,0,width,height); // Сбрасываем область просмотра
glMatrixMode(GL_PROJECTION); // Выбираем матрицу проекции
glLoadIdentity(); // Сбрасываем матрицу проекции
// Устанавливаем ортографическую проекцию 640x480 (0,0 - Верх Лево)
glOrtho(0.0f,640,480,0.0f,-1.0f,1.0f);
glMatrixMode(GL_MODELVIEW); // Выбираем матрицу просмотра модели
glLoadIdentity(); // Сбрасываем матрицу просмотра модели
}
Код инициализации очень мал. Мы загружаем наш TGA файл. Заметьте, что первым параметром передается &textures[0]. Второй параметр - имя файла, который мы хотим загрузить. В нашем случае, мы хотим загрузить файл Font.TGA. Если LoadTGA() вернет false по какой-то причине, выражение if также вернет false, что приведет к завершению программы с сообщением "initialization failed".
Если Вы захотите загрузить вторую текстуру, то Вы должны будете использовать следующий код: if ((!LoadTGA(&textures[0],"image1.tga")) || (!LoadTGA(&textures[1],"image2.tga"))) { }
После того как мы загрузили TGA (создали нашу текстуру), мы строим наш шрифт, устанавливаем плавное сглаживание, делаем фоновый цвет черным, разрешаем очистку буфера глубины и выбираем нашу текстуру шрифта (привязываемся к ней).
Наконец, мы возвращаем true, и тем самым даем знать нашей программе, что инициализация прошла ok.
int InitGL(Glvoid) // Все настройки для OpenGL
{
if (!LoadTGA(&textures[0],"Data/Font.TGA"))// Загружаем текстуру шрифта
{
return false; // Если ошибка, возвращаем false
}
BuildFont(); // Строим шрифт
glShadeModel(GL_SMOOTH); // Разрешаем плавное сглаживание
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Черный фон
glClearDepth(1.0f); // Устанавливаем буфер глубины
glBindTexture(GL_TEXTURE_2D, textures[0].texID); // Выбираем нашу текстуру шрифта
return TRUE; // Инициализация прошла OK
}
Код отрисовки совершенно новый :). Мы начинаем с того, что создаем переменную token типа char. Token будет хранить текст, разобранный далее в коде.
У нас есть другая переменная, названная cnt. Я использую эту переменную, как для подсчета количества поддерживаемых расширений, так и для позиционирования текста на экране. cnt сбрасывается в нуль каждый раз, когда мы вызываем DrawGLScene.
Мы очищаем экран и буфер глубины, затем устанавливаем цвет в ярко-красный (красный полной интенсивности, 50% зеленый, 50% синий). С позиции 50 по оси x и 16 по оси y мы выводим слово "Renderer". Также мы выводим "Vendor" и "Version" вверху экрана. Причина, по которой каждое слово начинается не с 50 по оси x, в том, что я выравниваю все эти слова по их правому краю (все они выстраиваются по правой стороне).
int DrawGLScene(GLvoid) // Здесь происходит все рисование
{
char *token; // Место для хранения расширений
int cnt=0; // Локальная переменная цикла
// Очищаем экран и буфер глубины
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glColor3f(1.0f,0.5f,0.5f); // Устанавливаем цвет в ярко-красный
glPrint(50,16,1,"Renderer"); // Выводим имя производителя
glPrint(80,48,1,"Vendor"); // Выводим имя поставщика
glPrint(66,80,1,"Version"); // Выводим версию карты
Теперь, когда у нас есть текст на экране, мы изменяем цвет на оранжевый, и считываем из видеокарты имя производителя, имя поставщика и номер версии видеокарты. Мы делаем это, передавая GL_RENDERER, GL_VENDOR и GL_VERSION в glGetString(). glGetString вернет запрошенные имя производителя, имя поставщика и номер версии. Возвращаемая информация будет текстом, поэтому мы должны запросить информацию от glGetString как char. Это значит, что мы сообщаем программе, что мы хотим, чтобы возвращаемая информация была символами (текст). Если Вы не включите (char *), то Вы получите сообщение об ошибке. Мы печатаем в текстовом виде, поэтому нам необходимо возвратить текст. Мы получаем все три части информации и выводим их справа от предыдущего текста.
Информация, которую мы получим от glGetString(GL_RENDERER), будет выведена рядом с красным текстом "Renderer", а информация, которую мы получим от glGetString(GL_VENDOR), будет выведена справа от "Vendor", и т.д.
Мне бы хотелось объяснить процесс приведения типа более подробно, но у меня нет по-настоящему хорошего объяснения. Если кто-то может хорошо объяснить это, напишите мне, и я изменю мои пояснения.
После того, как мы поместили информацию о производителе, имя поставщика и номер версии на экран, мы изменяем цвет на ярко-синий и выводим "NeHe Productions" в низу экрана :). Конечно, Вы можете изменить этот текст как угодно.
glColor3f(1.0f,0.7f,0.4f); // Устанавливаем цвет в оранжевый
glPrint(200,16,1,(char *)glGetString(GL_RENDERER));// Выводим имя производителя
glPrint(200,48,1,(char *)glGetString(GL_VENDOR)); // Выводим имя поставщика
glPrint(200,80,1,(char *)glGetString(GL_VERSION)); // Выводим версию
glColor3f(0.5f,0.5f,1.0f); // Устанавливаем цвет в ярко-голубой
glPrint(192,432,1,"NeHe Productions"); // Печатаем NeHe Productions в низу экрана
Сейчас мы нарисуем красивую белую рамку вокруг экрана и вокруг текста. Мы начнем со сброса матрицы просмотра модели. Поскольку мы напечатали текст на экране и находимся не в точке 0,0 экрана, это лучший способ для возврата в 0,0.
Затем мы устанавливаем цвет в белый и начинаем рисовать наши рамки. Ломаная линия достаточно легка в использовании. Мы говорим OpenGL, что хотим нарисовать ломаную линию с помощью glBegin(GL_LINE_STRIP). Затем мы устанавливаем первую вершину. Наша первая вершина будет находиться на краю правой части экрана и на 63 пиксела вверх от нижнего края экрана (639 по оси x и 417 по оси y). После этого мы устанавливаем вторую вершину. Мы остаемся в том же месте по оси y (417), но по оси x сдвигаемся на левый край (0). Линия будет нарисована из правой части экрана (639,417) в левую часть(0,417).
У Вас должно быть, по крайней мере, две вершины для рисования линии (как подсказывает здравый смысл). Из левой части экрана мы перемещаемся вниз, вправо, и затем вверх (128 по оси y).
Затем мы начинаем другую ломаную линию, и рисуем вторую рамку вверху экрана. Если Вам нужно нарисовать много соединенных линий, то ломаная линия определенно позволит снизить количество кода, который был бы необходим для рисования регулярных линий(GL_LINES).
glLoadIdentity(); // Сбрасываем матрицу просмотра модели
glColor3f(1.0f,1.0f,1.0f); // Устанавливаем цвет в белый
glBegin(GL_LINE_STRIP); // Начало рисования ломаной линии
glVertex2d(639,417); // Верх Право нижней рамки
glVertex2d( 0,417); // Верх Лево нижней рамки
glVertex2d( 0,480); // Низ Лево нижней рамки
glVertex2d(639,480); // Низ Право нижней рамки
glVertex2d(639,128); // Вверх к Низу Права верхней рамки
glEnd(); // Конец первой ломаной линии
glBegin(GL_LINE_STRIP); // Начало рисования другой ломаной линии
glVertex2d( 0,128); // Низ Лево верхней рамки
glVertex2d(639,128); // Низ Право верхней рамки
glVertex2d(639, 1); // Верх Право верхней рамки
glVertex2d( 0, 1); // Верх Лево верхней рамки
glVertex2d( 0,417); // Вниз к Верху Лева нижней рамки
glEnd(); // Конец второй ломаной линии
А теперь кое-что новое. Чудесная GL команда, которая называется glScissor(x,y,w,h). Эта команда создает почти то, что можно называть окном. Когда разрешен GL_SCISSOR_TEST, то единственной частью экрана, которую Вы можете изменять, является та часть, которая находится внутри вырезанного окна. Первая строка ниже создает вырезанное окно, начиная с 1 по оси x и 13.5% (0.135…f) пути снизу экрана по оси y. Вырезанное окно будет иметь 638 пикселов в ширину(swidth-2) и 59.7%(0.597…f) экрана в высоту.
В следующей строке мы разрешаем вырезку. Что бы мы ни рисовали за пределами вырезанного окна, не появится. Вы можете нарисовать ОГРОМНЫЙ четырехугольник на экране с 0,0 до 639,480, но Вы увидите только ту часть, которая попала в вырезанное окно. Оставшаяся часть экрана не будет видна. Замечательная команда!
Третья строка кода создает переменную text, которая будет хранить символы, возвращаемые glGetString(GL_EXTENSIONS). malloc(strlen((char *)glGetString(GL_EXTENSIONS))+1) выделяет достаточно памяти, для хранения всей строки, которая будет возвращена, + 1 (таким образом, если строка содержит 50 символов, то text будет в состоянии хранить все 50 символов).
Следующая строка копирует информацию GL_EXTENSIONS в text. Если мы непосредственно модифицируем информацию GL_EXTENSIONS, то возникнут большие проблемы, поэтому вместо этого мы копируем информацию в text, и затем манипулируем информацией, сохраненной в text. По сути, мы просто берем копию и сохраняем ее в переменной text.
// Определяем область вырезки
glScissor(1,int(0.135416f*sheight),swidth-2,int(0.597916f*sheight));
glEnable(GL_SCISSOR_TEST); // Разрешаем вырезку
// Выделяем память для строки расширений
char* text=(char*)malloc(strlen((char *)glGetString(GL_EXTENSIONS))+1);
// Получаем список расширений и сохраняем его в text
strcpy (text,(char *)glGetString(GL_EXTENSIONS));
Сейчас, немного нового. Давайте предположим, что после захвата информации о расширениях из видеокарты, в переменной text хранится следующая строка… "GL_ARB_multitexture GL_EXT_abgr GL_EXT_bgra". strtok(TextToAnalyze,TextToFind) будет сканировать переменную text пока не найдет в ней " "(пробел). Как только пробел будет найден, будет скопировано содержимое text ВПЛОТЬ ДО пробела в переменную token. В нашем случае, token будет равняться "GL_ARB_multitexture". Затем пробел заменится маркером. Подробнее об этом через минуту.
Далее мы создаем цикл, который остановится когда больше не останется информации в text. Если в text нет информации, то token будет равняться NULL и цикл остановится.
Мы увеличиваем переменную счетчик (cnt) на единицу, и затем проверяем больше ли значение cnt чем maxtokens. Если cnt больше чем maxtokens, то мы приравниваем maxtokens к cnt. Таким образом, если счетчик достиг 20-ти, то maxtokens будет также равен 20. Это необходимо для того, чтобы следить за максимальным значением счетчика.
// Разбиваем 'text' на слова, разделенные " " (пробелом)
token=strtok(text," ");
while(token!=NULL) // Пока token не NULL
{
cnt++; // Увеличиваем счетчик
if (cnt>maxtokens) // Если 'maxtokens' меньше или равно 'cnt'
{
maxtokens=cnt; // Если да, то 'maxtokens' приравниваем к 'cnt'
}
Итак, мы сохранили первое расширение из нашего списка расширений в переменную token. Следующее, что мы сделаем - установим цвет в ярко-зеленый. Затем мы напечатаем переменную cnt в левой части экрана. Заметьте, что мы печатаем с позиции 0 по оси x. Это могло бы удалить левую (белую) рамку, которую мы нарисовали, но так как включена вырезка, то пиксели, нарисованные в 0 по оси x, не будут изменены. Не получится нарисовать поверх рамки.
Переменная будет выведена левого края экрана (0 по оси x). По оси y мы начинаем рисовать с 96. Для того чтобы весь текст не выводился в одной и той же точке экрана, мы добавил (cnt*32) к 96. Так, если мы отображаем первое расширение, то cnt будет равно 1, и текст будет нарисован с 96+(32*1)(128) по оси y. Если мы отображаем второе расширение, то cnt будет равно 2, и текст будет нарисован с 96+(32*2)(160) по оси y.
Заметьте, что я всегда вычитаю scroll. Во время запуска программы scroll будет равняться 0. Так, наша первая строка текста рисуется с 96+(32*1)-0. Если Вы нажмете СТРЕЛКА ВНИЗ, то scroll увеличится на 2. Если scroll равняется 4, то текст будет нарисован с 96+(32*1)-4. Это значит, что текст будет нарисован с 124 вместо 128 по оси y, поскольку scroll равняется 4. Верх нашего вырезанного окна заканчивается в 128 по оси y. Любая часть текста, рисуемая в строках 124-127 по оси y, не появится на экране.
Тоже самое и с низом экрана. Если cnt равняется 11 и scroll равняется 0, то текст должен быть нарисован с 96+(32*11)-0 и в 448 по оси y. Поскольку вырезанное окно позволяет нам рисовать только до 416 по оси y, то текст не появится вообще.
Последнее, что нам нужно от прокручиваемого окна, это возможность посматривать 288/32 (9) строк текста. 288 - это высота нашего вырезанного окна. 32 - высота нашего текста. Изменяя значение scroll, мы можем двигать текст вверх или вниз (смещать текст).
Эффект подобен кинопроектору. Фильм прокручивается через линзу и все, что Вы видите - текущий кадр. Вы не видите область выше или ниже кадра. Линза выступает в качестве окна, аналогично окну, созданному при помощи вырезки.
После того, как мы нарисовали текущий счетчик (сnt) на экране, изменяем цвет на желтый, передвигаемся на 50 пикселов вправо по оси x, и выводим текст, хранящийся в переменной token на экран.
Используя наш пример выше, первая строка текста, которая будет отображена на экране, будет иметь вид:
1 GL_ARB_multitexture
glColor3f(0.5f,1.0f,0.5f); // Устанавливаем цвет в ярко-зеленый
glPrint(0,96+(cnt*32)-scroll,0,"%i",cnt); // Печатаем текущий номер расширения
glColor3f(1.0f,1.0f,0.5f); // Устанавливаем цвет в желтый
glPrint(50,96+(cnt*32)-scroll,0,token); // Печатаем текущее расширение
После того, как мы отобразили значение token на экране, мы должны проверить переменную text: есть ли еще поддерживаемые расширения. Вместо использования token=strtok(text," "), как мы делали выше, мы замещаем text на NULL. Это сообщает команде strtok, что искать нужно от последнего маркера до СЛЕДУЮЩЕГО пробела в строке текста (text).
В нашем примере выше ("GL_ARB_multitexturemarkerGL_EXT_abgr GL_EXT_bgra") маркер будет находиться после текста "GL_ARB_multitexture". Строка кода ниже начнет поиск ОТ маркера до следующего пробела. Все, находящееся от маркера до следующего пробела, будет сохранено в token. В token будет помещено "GL_EXT_abgr", в text будет храниться GL_ARB_multitexturemarkerGL_EXT_abgrmarkerGL_EXT_bgra".
Когда у strtok() не останется текста для сохранения в token, token станет равным NULL и цикл остановится.
token=strtok(NULL," "); // Поиск следующего расширения
}
После того, как все расширения будут разобраны из переменной text, мы можем запретить вырезку и освободить переменную text. Это освобождает память, которую мы использовали для хранения информации, полученной от glGetString(GL_EXTENSIONS).
При следующем вызове DrawGLScene(), будет выделена новая память. Свежая копия информации, которую вернет glGetStrings(GL_EXTENSIONS), будет скопирована с переменную text и весь процесс начнется заново.
glDisable(GL_SCISSOR_TEST); // Запрещаем вырезку
free (text); // Освобождаем выделенную память
Первая строка ниже необязательна, но я подумал, что это хороший случай рассказать о ней, чтобы все знали, что она существует. Команда glFlush() в основном говорит OpenGL закончить то, что он делает. Если Вы когда-нибудь заметите мерцание в Вашей программе (исчезновение четырехугольников, и т.д.), то попытайтесь добавить команду flush в конец DrawGLScene.
Последнее, что мы делаем - возвращаем true, чтобы показать, что все прошло ok.
glFlush(); // Сброс конвейера рендеринга
return TRUE: // Все прошло ok
}
Единственно, что стоит отметить в KillGLWindow() - в конец я добавил KillFont(). Таким образом, когда окно будет уничтожено, шрифт также будет уничтожен.
GLvoid KillGLWindow(GLvoid) // Правильное уничтожение окна
{
if (fullscreen) // Полноэкранный режим?
{
ChangeDisplaySettings(NULL,0); // Переход в режим разрешения рабочего стола
ShowCursor(TRUE);// Показать указатель мыши
}
if (hRC) // Существует контекст рендеринга?
{
if (!wglMakeCurrent(NULL,NULL)) // Можно ли освободить DC и RC контексты?
{
MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",
MB_OK | MB_ICONINFORMATION);
}
if (!wglDeleteContext(hRC)) // Можно ли уничтожить RC?
{
MessageBox(NULL,"Release Rendering Context Failed.",
"SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
}
hRC=NULL; // Установим RC в NULL
}
if (hDC && !ReleaseDC(hWnd,hDC)) // Можно ли уничтожить DC?
{
MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",
MB_OK | MB_ICONINFORMATION);
hDC=NULL; // Установим DC в NULL
}
if (hWnd && !DestroyWindow(hWnd)) // Можно ли уничтожить окно?
{
MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",MB_OK |
MB_ICONINFORMATION);
hWnd=NULL; // Уствновим hWnd в NULL
}
if (!UnregisterClass("OpenGL",hInstance)) // Можно ли уничтожить класс?
{
MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK |
MB_ICONINFORMATION);
hInstance=NULL; // Устанавливаем hInstance в NULL
}
KillFont(); // Уничтожаем шрифт
}
CreateGLWindow(), и WndProc() - те же.
Первое изменение в WinMain() - название, которое появляется вверху окна. Теперь оно будет "NeHe's Extensions, Scissoring, Token & TGA Loading Tutorial".
int WINAPI WinMain(HINSTANCE hInstance, // Экземпляр
HINSTANCE hPrevInstance, // Предыдущий экземпляр
LPSTR lpCmdLine, // Параметры командной строки
Int nCmdShow) // Состояние окна
{
MSG msg; // Структура сообщения Windows
BOOL done=FALSE; // Логическая переменная выхода из цикла
// Спрашиваем у юзера какой режим он предпочитает
if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?",
"Start FullScreen?", MB_YESNO | MB_ICONQUESTION)==IDNO)
{
fullscreen=FALSE; // Оконный режим
}
// Создание OpenGL окна
if (!CreateGLWindow("NeHe's Token, Extensions, Scissoring & TGA Loading
Tutorial",640,480,16,fullscreen))
{
return 0; // Выход, если окно не было создано
}
while(!done) // Цикл выполняется пока done=FALSE
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Есть сообщение?
{
if (msg.message==WM_QUIT) // Получили сообщение Quit?
{
done=TRUE; // Если да, то done=TRUE
}
else // Если нет, то обрабатываем оконные сообщения
{
DispatchMessage(&msg); // Отправляем сообщение
}
}
else // Если нет сообщений
{
// Рисуем сцену. Проверяем клавишу ESC и сообщение QUIT из DrawGLScene()
// Активно? Получили Quit сообщение?
if ((active && !DrawGLScene()) || keys[VK_ESCAPE])
{
done=TRUE; // ESC или DrawGLScene сигнализирует о выходе
}
else // Не время выходить, обновляем экран
{
SwapBuffers(hDC); // Меняем буфера (двойная буферизация)
if (keys[VK_F1]) // Нажата клавиша F1?
{
keys[VK_F1]=FALSE; // Если да, то установим в FALSE
KillGLWindow(); // Уничтожаем наше текущее окно
fullscreen=!fullscreen; // Полноэкран./окон. режим
// Создаем наше OpenGL окно
if (!CreateGLWindow("NeHe's Token, Extensions,
Scissoring & TGA Loading Tutorial", 640,480,16,fullscreen))
{
return 0; // Выход если окно не было создано
}
}
Код ниже проверяет: если была нажата стрелка вверх и scroll больше 0, то уменьшаем scroll на 2. Это заставляет сдвинуться вниз текст на экране.
if (keys[VK_UP] && (scroll>0)) // Нажата стрелка вверх?
{
scroll-=2; // Если да, то уменьшаем 'scroll', двигая экран вниз
}
Если была нажата стрелка вниз, и scroll меньше чем (32*(maxtokens-9)), то scroll будет увеличена на 2, и текст на экране сдвинется вверх.
32 - это количество пикселей, занимаемое каждой строкой. Maxtokens - общее количество расширений, поддерживаемых Вашей видеокартой. Мы вычитаем 9, поскольку 9 строк могут одновременно показываться на экране. Если мы не вычтем 9, то сможем сдвигать за пределы списка, что приведет к тому, что список полностью сдвинется за пределы экрана. Попробуйте убрать -9, если Вы не понимаете, о чем я говорю.
if (keys[VK_DOWN] && (scroll<32*(maxtokens-9))) // Нажата стрелка вниз?
{
scroll+=2; // Если да, то увеличиваем 'scroll', двигая экран вверх
}
}
}
}
// Завершение
KillGLWindow(); // Уничтожаем окно
return (msg.wParam); // Выходим из программы
}
Я надеюсь, что Вы нашли этот урок интересным. К концу этого урока Вы должны знать, как считывать имя производителя, поставщика и номер версии из Вашей видеокарты. Также Вы должны уметь определять, какие расширения поддерживаются любой видеокартой, которая поддерживает OpenGL. Также Вы должны знать, что такое вырезка и как можно использовать ее в своих OpenGL проектах, и, наконец, Вы должны знать, как загружать TGA изображения вместо BMP для использования их в качестве текстур.
Если у Вас возникли какие-либо проблемы с этим уроком, или Вы находите, что эта информация трудна для понимания, дайте мне знать. Я хочу сделать свои уроки лучше, насколько это возможно. Обратная связь очень важна!
© Jeff Molofee (NeHe)
20 ноября 2002 (c) Popov Denis