Текстурные шрифты
Texture Mapped Fonts
После публикации двух последних уроков о растровых и векторных шрифтах, я получил несколько писем от людей, которые задают вопрос: можно ли накладывать текстуру на шрифт. Вы можете использовать автоматическую генерацию координат текстуры. При этом будут генерироваться координаты текстуры для каждого полигона у шрифта.
Небольшое примечание, этот код применим только в Windows. Здесь используются функции wgl Windows для построения шрифта. Очевидно, Apple имеет поддержку agl, которая должна делать тоже самое, и X имеет glx. К сожалению, я не могу гарантировать, что этот код переносим. Если кто-нибудь имеет платформо-незавизимый код для вывода шрифтов на экран, пришлите мне его, и я напишу другой урок по шрифтам.
Мы будем использовать код от урока 14 для нашей демонстрационной программы текстурных шрифтов. Если код изменился в каком-либо разделе программы, я перепишу весь раздел кода, чтобы было проще видеть изменения, которые я сделал.
Следующий раздел кода такой же как в уроке 14, но на этот раз мы не включим в него stdarg.h.
#include <windows.h> // Заголовочный файл для Windows
#include <stdio.h> // Заголовочный файл для стандартной библиотеки ввода/вывода
#include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32
#include <gl\glu.h> // Заголовочный файл для библиотеки GLu32
#include <gl\glaux.h> // Заголовочный файл для библиотеки GLaux
#include <math.h> // Заголовочный файл для математической библиотеки
HDC hDC=NULL; // Приватный контекст устройства GDI
HGLRC hRC=NULL; // Постоянный контекст рендеринга
HWND hWnd=NULL; // Сохраняет дескриптор окна
HINSTANCE hInstance; // Сохраняет экземпляр приложения
bool keys[256]; // Массив для работы с клавиатурой
bool active=TRUE; // Флаг активации окна, по умолчанию = TRUE
bool fullscreen=TRUE;// Флаг полноэкранного режима
Мы собираемся добавить одну новую переменную целого типа, которая называется texture[]. Она будет использоваться для хранения нашей текстуры. Последние три строки такие же, как в уроке 14 и не изменились и здесь.
GLuint texture[1]; // Одна текстура ( НОВОЕ )
GLuint base; // База списка отображения для фонта
GLfloat rot; // Используется для вращения текста
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление WndProc
Следующий раздел кода претерпел незначительные изменения. В этом уроке я собираюсь использовать wingdings шрифт, для того чтобы отобразить объект в виде черепа и двух скрещенных костей под ним (эмблема смерти). Если Вы захотите вывести текст вместо этого, Вы можете оставить код таким же, как это было в уроке 14, или замените шрифт на ваш собственный.
Может быть кто-то уже задавался вопросом, как использовать wingdings шрифт. Это тоже является причиной, по которой я не использую стандартный шрифт. wingdings – СПЕЦИАЛЬНЫЙ шрифт, и требует некоторых модификаций, чтобы заставить программу работать с ним. При этом надо не просто сообщить Windows, чтобы Вы будете использовать wingdings шрифт. Если Вы изменяете, название шрифта на wingdings, Вы увидите, что шрифт не будет выбран. Вы должны сообщить Windows, что шрифт является специальным шрифтом, и не стандартным символьным шрифтом. Но об этом позже.
GLvoid BuildFont(GLvoid) // Построение шрифта
{
GLYPHMETRICSFLOAT gmf[256]; // Адрес буфера для хранения шрифта
HFONT font; // ID шрифта в Windows
base = glGenLists(256); // Храним 256 символов
font = CreateFont( -12, // Высота фонта
0, // Ширина фонта
0, // Угол отношения
0, // Угол наклона
FW_BOLD, // Ширина шрифта
FALSE, // Курсив
FALSE, // Подчеркивание
FALSE, // Перечеркивание
Вот она волшебная строка! Вместо того чтобы использовать ANSI_CHARSET, как мы делали в уроке 14, мы используем SYMBOL_CHARSET. Это говорит Windows, что шрифт, который мы строим - не обычный шрифт, составленный из букв. Специальный шрифт обычно составлен из крошечных картинок (символов). Если Вы забудете изменить эту строку, wingdings, webdings и любой другой специальный шрифт, который Вы можете пробовать использовать, не будет работать.
SYMBOL_CHARSET, // Идентификатор набора символов ( Модифицировано )
Следующие строки не изменились.
OUT_TT_PRECIS, // Точность вывода
CLIP_DEFAULT_PRECIS, // Точность отсечения
ANTIALIASED_QUALITY, // Качество вывода
FF_DONTCARE|DEFAULT_PITCH, // Семейство и шаг
Теперь, когда мы выбрали идентификатор набора символов, мы можем выбирать wingdings шрифт!
"Wingdings"); // Имя шрифта ( Модифицировано )
SelectObject(hDC, font); // Выбрать шрифт, созданный нами
wglUseFontOutlines( hDC, // Выбрать текущий контекст устройства (DC)
0, // Стартовый символ
255, // Количество создаваемых списков отображения
base, // Стартовое значение списка отображения
Я устанавливаю большой уровень отклонения. При этом GL не будет точно отслеживать контур шрифта. Если Вы зададите отклонение равным 0.0f, Вы заметите проблемы с текстурированием на очень изогнутых поверхностях. Если Вы допустите некоторое отклонение, большинство проблем исчезнет.
0.1f, // Отклонение от истинного контура
Следующие три строки кода те же самые.
0.2f, // Толщина шрифта по оси Z
WGL_FONT_POLYGONS, // Использовать полигоны, а не линии
gmf); // Буфер адреса для данных списка отображения
}
Перед ReSizeGLScene() мы собираемся добавить следующий раздел кода для загрузки нашей текстуры. Вы знаете этот код по прошлым урокам. Мы создаем память для хранения растрового изображения. Мы загружаем растровое изображение. Мы говорим OpenGL, сгенерировать 1 текстуру, и мы сохраняем эту текстуру в texture[0].
Я создаю мип-мап текстуру, так как она смотрится лучше. Имя текстуры - lights.bmp.
AUX_RGBImageRec *LoadBMP(char *Filename) // Загрузка картинки
{
FILE *File=NULL; // Индекс файла
if (!Filename) // Проверка имени файла
{
return NULL; // Если нет вернем NULL
}
File=fopen(Filename,"r"); // Проверим существует ли файл
if (File) // Файл существует?
{
fclose(File); // Закрыть файл
return auxDIBImageLoad(Filename); // Загрузка картинки и вернем на нее указатель
}
return NULL; // Если загрузка не удалась вернем NULL
}
int LoadGLTextures() // Загрузка картинки и конвертирование в текстуру
{
int Status=FALSE; // Индикатор состояния
AUX_RGBImageRec *TextureImage[1]; // Создать место для текстуры
memset(TextureImage,0,sizeof(void *)*1); // Установить указатель в NULL
// Загрузка картинки, проверка на ошибки, если картинка не найдена - выход
if (TextureImage[0]=LoadBMP("Data/Lights.bmp"))
{
Status=TRUE; // Установим Status в TRUE
glGenTextures(1, &texture[0]); // Создание трех текстур
// Создание текстуры с мип-мап наложением
glBindTexture(GL_TEXTURE_2D, texture[0]);
gluBuild2DMipmaps(GL_TEXTURE_2D, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY,
GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_NEAREST);
}
В следующих четырех строках кода автоматически генерируются текстурные координаты для любого объекта, который мы выводим на экран. Команда glTexGen чрезвычайно мощная и комплексная, и включает достаточно сложную математику. Но Вы должны только знать то, что GL_S и GL_T - координаты текстуры. По умолчанию они заданы так, чтобы брать текущее x положение на экране и текущее y положение на экране и из них вычислить вершину текстуру. Вы можете отметить, что объекты не текстурированны по z плоскости... только появляются полосы. Передние и задние грани текстурированны, однако, именно это и необходимо. X (GL_S) охватывает наложение текстуры слева направо, а Y (GL_T) охватывает наложение текстуры сверху и вниз.
GL_TEXTURE_GEN_MODE позволяет нам выбрать режим наложения текстуры, который мы хотим использовать по координатам текстуры S и T. Есть три возможности:
GL_EYE_LINEAR - текстура зафиксирована на экране. Она никогда не перемещается. Объект накладывается на любую часть текстуры, которую он захватывает.
GL_OBJECT_LINEAR – мы воспользуемся этим режимом. Текстура привязана к объекту, перемещающемуся по экрану.
GL_SPHERE_MAP – всегда в фаворе. Создает металлический отражающий тип объекта.
Важно обратить внимание на то, что я опускаю много кода. Мы также должны задать GL_OBJECT_PLANE, но значение по умолчанию то, которое мы хотим. Купите хорошую книгу, если Вы хотите изучить больше, или поищите в помощи MSDN на CD.
// Текстуризация контура закрепленного за объектом
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
// Текстуризация контура закрепленного за объектом
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR);
glEnable(GL_TEXTURE_GEN_S); // Автоматическая генерация
glEnable(GL_TEXTURE_GEN_T); // Автоматическая генерация
}
if (TextureImage[0]) // Если текстура существует
{
if (TextureImage[0]->data) // Если изображение текстуры существует
{
free(TextureImage[0]->data); // Освобождение памяти изображения текстуры
}
free(TextureImage[0]); // Освобождение памяти под структуру
}
return Status; // Возвращаем статус
}
Есть несколько новых строк кода в конце InitGL(). Вызов BuildFont() был помещен ниже кода, загружающего нашу текстуру. Строка с glEnable(GL_COLOR_MATERIAL) была удалена. Если Вы хотите задать текстуре цвет, используйте glColor3f(r, г, b) и добавьте строку glEnable(GL_COLOR_MATERIAL) в конце этой секции кода.
int InitGL(GLvoid) // Все начальные настройки OpenGL здесь
{
if (!LoadGLTextures()) // Переход на процедуру загрузки текстуры
{
return FALSE; // Если текстура не загружена возвращаем FALSE
}
BuildFont(); // Построить шрифт
glShadeModel(GL_SMOOTH); // Разрешить плавное затенение
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Черный фон
glClearDepth(1.0f); // Установка буфера глубины
glEnable(GL_DEPTH_TEST); // Разрешение теста глубины
glDepthFunc(GL_LEQUAL); // Тип теста глубины
glEnable(GL_LIGHT0); // Быстрое простое освещение
// (устанавливает в качестве источника освещения Light0)
glEnable(GL_LIGHTING); // Включает освещение
// Действительно хорошие вычисления перспективы
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
Разрешение наложения 2D текстуры, и выбор текстуры номер один. При этом будет отображена текстура номер один на любой 3D объект, который мы выводим на экран. Если Вы хотите большего контроля, Вы можете разрешать и запрещать наложение текстуры самостоятельно.
glEnable(GL_TEXTURE_2D); // Разрешение наложения текстуры
glBindTexture(GL_TEXTURE_2D, texture[0]); // Выбор текстуры
return TRUE; // Инициализация окончена успешно
}
Код изменения размера не изменился, но код DrawGLScene изменился.
int DrawGLScene(GLvoid) // Здесь мы будем рисовать все
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экран и буфера глубины
glLoadIdentity(); // Сброс просмотра
Здесь наше первое изменение. Вместо того чтобы поместить объект в середину экрана, мы собираемся вращать его на экране, используя COS и SIN (это не сюрприз). Мы перемещаемся на 3 единицы в экран (-3.0f). По оси X, мы будем раскачиваться от -1.1 слева до +1.1 вправо. Мы будем использовать переменную rot для управления раскачиванием слева направо. Мы будем раскачивать от +0.8 верх до -0.8 вниз. Мы будем использовать переменную rot для этого раскачивания также (можно также задействовать и другие переменные).
// Позиция текста
glTranslatef(1.1f*float(cos(rot/16.0f)),0.8f*float(sin(rot/20.0f)),-3.0f);
Теперь сделаем вращения. Символ будет вращаться по осям X, Y и Z.
glRotatef(rot,1.0f,0.0f,0.0f); // Вращение по оси X
glRotatef(rot*1.2f,0.0f,1.0f,0.0f); // Вращение по оси Y
glRotatef(rot*1.4f,0.0f,0.0f,1.0f); // Вращение по оси Z
Мы смещаем символ по каждой оси немного налево, вниз, и вперед, чтобы центрировать его. Иначе, когда он вращается, он будет вращаться не вокруг собственного центра. (-0.35f, -0.35f, 0.1f) те числа, которые подходят. Я потратил некоторое время, прежде чем подобрал их, и они могут изменяться в зависимости от шрифта. Почему шрифты построены не вокруг центральной точки, я не знаю.
glTranslatef(-0.35f,-0.35f,0.1f); // Центр по осям X, Y, Z
Наконец мы выводим наш эмблемы смерти, затем увеличиваем переменную rot, поэтому наш символ вращается и перемещается по экрану. Если Вы не можете понять, почему я получаю череп из символа 'N', сделайте так: запустите Microsoft Word или Wordpad. Вызовите ниспадающие меню шрифтов. Выберите wingdings шрифт. Наберите в верхнем регистре 'N'. Появиться эмблема смерти.
glPrint("N"); // Нарисуем символ эмблемы смерти
rot+=0.1f; // Увеличим переменную вращения
return TRUE; // Покидаем эту процедуру
}
Последнее, что надо сделать добавить KillFont() в конце KillGLWindow() точно так, как показано ниже. Важно добавить эту строку. Это почистит память прежде, чем мы выйдем из нашей программы.
if (!UnregisterClass("OpenGL",hInstance)) // Если класс не зарегистрирован
{
MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION);
hInstance=NULL; // Установить копию приложения в ноль
}
KillFont(); // Уничтожить шрифт
}
Даже притом, что я не вдавался в излишние подробности, Вы должны получить хорошее понимание о том, как заставить OpenGL генерировать координаты текстуры. У Вас не должно возникнуть никаких проблем при наложении текстур на шрифты, или даже на другие объекты. И, изменяя только две строки кода, Вы можете разрешить сферическое наложение, которое является действительно крутым эффектом.
по OpenGL. Эффект тумана на OpenGL
Cool Looking Fog
Этот урок представляет Крис Алиотта (Chris Aliotta)...
Итак, Вы хотите добавить туман в Вашу программу на OpenGL? Что ж, в этом уроке я покажу как сделать именно это. Я первый раз пишу урок, и относительно недавно познакомился с программированием на OpenGL/C++, так что пожалуйста, если вы найдете здесь какие-нибудь ошибки, то пожалуйста, сообщите мне и не накидывайтесь все сразу. Эта программа основана на примере седьмого урока.
Подготовка данных:
Начнем с того, что подготовим все необходимые переменные, содержащие параметры затуманивания. Массив fogMode будет хранить три значения: GL_EXP, GL_EXP2 и GL_LINEAR, - это три типа тумана. Позже я объясню различия между ними. Объявим переменные в начале кода, после строки GLuint texture[3]. В переменной fogfilter будет храниться какой тип тумана мы будем использовать, fogColor будет содержать цвет, который мы хотим придать туману. Еще я добавил двоичную переменную gp в начало кода, чтобы можно было узнать нажата ли клавиша 'g' во время выполнения программы-примера.
bool gp; // G Нажата? ( Новое )
GLuint filter; // Используемый фильтр для текстур
GLuint fogMode[]= { GL_EXP, GL_EXP2, GL_LINEAR }; // Хранит три типа тумана
GLuint fogfilter= 0; // Тип используемого тумана
GLfloat fogColor[4]= {0.5f, 0.5f, 0.5f, 1.0f}; // Цвет тумана
Изменения в DrawGLScene
Теперь, когда мы задали переменные, передвинемся вниз к функции InitGL. Строка glClearColor() изменена так, чтобы заполнить экран цветом тумана для достижения лучшего эффекта. Требуется не так уж много кода, чтобы задействовать туман. Вообщем, Вам все это покажется очень простым.
glClearColor(0.5f,0.5f,0.5f,1.0f); // Будем очищать экран, заполняя его цветом тумана. ( Изменено )
glEnable(GL_FOG); // Включает туман (GL_FOG)
glFogi(GL_FOG_MODE, fogMode[fogfilter]);// Выбираем тип тумана
glFogfv(GL_FOG_COLOR, fogColor); // Устанавливаем цвет тумана
glFogf(GL_FOG_DENSITY, 0.35f); // Насколько густым будет туман
glHint(GL_FOG_HINT, GL_DONT_CARE); // Вспомогательная установка тумана
glFogf(GL_FOG_START, 1.0f); // Глубина, с которой начинается туман
glFogf(GL_FOG_END, 5.0f); // Глубина, где туман заканчивается.
Возьмем сначала первые три строчки этого кода. Первая строка glEnable(GL_FOG) во многом говорит сама за себя. Ее задача - инициализировать туман.
Вторая строка, glFogi(GL_FOG_MODE, fogMode[fogfilter]) устанавливает режим фильтрации тумана. Ранее мы объявили массив fogMode. Он содержал GL_EXP, GL_EXP2 и GL_LINEAR. Именно здесь эти переменные входят в игру. Вот что каждая из них значит:
GL_EXP - Обычный туман, заполняющий весь экран. Во многом он напоминает туман отдаленно, но легко справляется со своей работой даже на старых PC.
GL_EXP2 - Это следующий шаг после GL_EXP. Затуманит весь экран, за то придает больше глубины всей сцене.
GL_LINEAR - Это лучший режим прорисовки тумана. Объекты выходят из тумана и исчезают в нем гораздо лучше.
Третья, glFogfv(GL_FOG_COLOR, fogcolor) задает цвет тумана. Раньше мы задали его как (0.5f,0.5f,0.5f,1.0f) через переменную fogcolor - получился приятный серый цвет.
Дальше, посмотрим на четыре последних строки. Строка glFogf(GL_FOG_DENSITY, 0.35f) устанавливает, насколько густым будет туман. Увеличьте число, и туман станет более густым, уменьшите - менее густым.
Eric Desrosiers добавляет небольшое объяснение glHint(GL_FOG_HINT, hintval):
hintval может быть: GL_DONT_CARE, GL_NICEST или GL_FASTEST
GL_DONT_CARE - позволяет OpenGL выбрать формулу для расчета тумана (по вершинам или по пикселям).
GL_NICEST - Создает туман по пикселям (хорошо смотрится).
GL_FASTEST - Вычисляет туман по вершинам (быстрее, но не так красиво)) .
Следующая строка glFogf(GL_FOG_START, 1.0f) устанавливает насколько близко к экрану начинается затуманивание. Вы можете изменить это число на что угодно, в зависимости от того, где бы Вы хотели, чтобы начался туман. Следующая, похожая, строка glFogf(GL_FOG_END, 5.0f) сообщает программе OpenGL насколько глубоко в экран должен уходить туман.
События при нажатии клавиш
Сейчас, когда код прорисовки уже готов, мы добавим команды для клавиатуры, чтобы переключаться между разными способами затуманивания. Этот код идет в конце программы, вместе с обработкой нажатия клавиш.
if (keys['G'] && !gp) // Нажата ли клавиша "G"?
{
gp=TRUE; // gp устанавливаем в TRUE
fogfilter+=1; // Увеличиние fogfilter на 1
if (fogfilter>2) // fogfilter больше 2 ... ?
{
fogfilter=0; // Если так, установить fogfilter в ноль
}
glFogi (GL_FOG_MODE, fogMode[fogfilter]); // Режим тумана
}
if (!keys['G']) // Клавиша "G" отпущена?
{
gp=FALSE; // Если да, gp установить в FALSE
}
Вот и все! Мы закончили. В Ваших программах с OpenGL есть туман. Я бы даже сказал, что это было достаточно безболезненно. Если есть какие вопросы или комментарии, легко можете со мной связаться: chris@incinerated.com. Так же заходите ко мне на сайт: http://www.incinerated.com/ и http://www.incinerated.com/precursor.
по OpenGL. Двухмерные шрифты из текстур
2D Texture Font
Этот урок написан NeHe & Giuseppe D'Agat.
Я знаю, что все устали от шрифтов. Те уроки, которые уже были рассмотрены ранее, не только показывали текст, но они отображали 3-х мерный текст, текстурированный текст, и могли быть привязаны к переменным. Но что будет, если вы перенесете свой проект на машину, которая не поддерживает Bitmap или Outline шрифты?
Благодаря Guiseppe D'Agata у нас есть еще один урок со шрифтами. Вы спросите, что же еще осталось? Если вы помните, в первом уроке про шрифты, я упоминал об использовании текстур для рисования букв на экран. Обычно, когда вы используете текстуры для рисования текста на экране, вы загружаете свою любимую программу рисования, выбираете шрифт и набираете букву или фразу, которую хотите отобразить на экране. Дальше вы сохраняете изображение и загружаете его в свою программу, как текстуру. Это не очень эффективно для программ, которые используют большое количество текста, или текст, который непрерывно меняется!
Эта программа использует только одну текстуру для отображения любого из 256 различных символов на экран. Имейте в виду, что каждый символ всего лишь 16 пикселов в ширину и 16 в высоту. Если взять стандартную текстуру 256*256, легко заметить, что в ней можно разместить только 16 символов поперек и получится 16 строк. Если нужно более детальное объяснение то: текстура 256 пикселов в ширину, а символ 16 пикселов в ширину. 256 делим на 16, получаем 16 :)
Итак. Давайте сделаем демонстрационную программу 2-х мерных шрифтов. Эта программа дополняет код из первого урока. В первой части программы мы включим библиотеки math и stdio. Математическая библиотека нужна, чтобы двигать буквы по экрану, используя синус и косинус, а библиотека stdio нужна, чтобы убедиться в том, что файлы картинок, которые мы загружаем действительно существуют, перед тем как мы попытаемся сделать из них текстуры.
#include <windows.h> // Заголовочный файл для Windows
#include <math.h> // Заголовочный файл для математической
// библиотеки Windows (Добавлено)
#include <stdio.h> // Заголовочный файл для стандартной библиотеки
//ввода/вывода (Добавлено)
#include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32
#include <gl\glu.h> // Заголовочный файл для библиотеки GLu32
#include <gl\glaux.h> // Заголовочный файл для библиотеки GLaux
HDC hDC=NULL; // Приватный контекст устройства GDI
HGLRC hRC=NULL; // Постоянный контекст визуализации
HWND hWnd=NULL; // Сохраняет дескриптор окна
HINSTANCE hInstance; // Сохраняет экземпляр приложения
bool keys[256]; // Массив для работы с клавиатурой
bool active=TRUE; // Флаг активации окна, по умолчанию = TRUE
bool fullscreen=TRUE; // Флаг полноэкранного вывода
Сейчас мы добавим переменную base указывающую на наш список отображения. Так же мы добавим texture[2], для хранения 2-х текстур, используемых для создания простого 3-х мерного объекта.
Добавим также переменную loop, которую будем использовать для циклов. И, наконец, cnt1 и cnt2, которые будут использоваться для движения текста по экрану и для вращения простого 3-х мерного объекта.
GLuint base; // Основной список отображения для шрифта
GLuint texture[2]; // Место для текстуры нашего шрифта
GLuint loop; // Общая переменная для циклов
GLfloat cnt1; // Первый счетчик для движения и раскрашивания текста
GLfloat cnt2; // Второй счетчик для движения и раскрашивания текста
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление WndProc
Теперь код для загрузки текстуры. Он точно такой же, как в предыдущих уроках по текстурированию.
AUX_RGBImageRec *LoadBMP(char *Filename) // Загрузка изображения
{
FILE *File=NULL; // Дескриптор файла
if (!Filename) // Удостоверимся, что имя файла передано
{
return NULL; // Если нет, возвратим NULL
}
File=fopen(Filename,"r"); // Проверка, существует ли файл
if (File) // Существует?
{
fclose(File); // Закрываем файл
// Загружаем изображение и возвращаем указатель
return auxDIBImageLoad(Filename);
}
return NULL; // Если загрузка не удалась, возвращаем NULL
}
Данный код тоже немного изменился, в отличие от кода в предыдущих уроках. Если вы не уверены в том, для чего каждая строка, вернитесь и просмотрите предыдущие примеры заново.
Отметим, что TextureImage[] будет хранить 2 записи о rgb изображении. Очень важно дважды проверить код, который работает с загрузкой и сохранением текстур. Одно неверное число может привести к зависанию!
int LoadGLTextures() // Загрузка и преобразование текстур
{
int Status=FALSE; // Индикатор статуса
AUX_RGBImageRec *TextureImage[2]; // Место хранения для текстур
Следующая строка самая важная. Если изменить 2 на любое другое число, точно возникнут проблемы. Проверьте дважды! Это число должно совпадать с тем, которое вы используете, когда определяете TextureImage[].
Две текстуры, которые мы загрузим, будут font.bmp (наш шрифт) и bumps.bmp. Вторая текстура может быть любой, какую вы захотите. Я не очень творческий человек, поэтому я решил воспользоваться простой текстурой.
memset(TextureImage,0,sizeof(void *)*2); // Устанавливаем указатель в NULL
if ((TextureImage[0]=LoadBMP("Data/Font.bmp")) &&// Загружаем изображение шрифта (TextureImage[1]=LoadBMP("Data/Bumps.bmp"))) // Загружаем текстуру
{
Status=TRUE; // Устанавливаем статус в TRUE
Другая важная строка, на которую нужно посмотреть дважды. Я не могу сказать, сколько писем я получил от людей, спрашивавших "почему я вижу только одну текстуру, или почему моя текстура вся белая!?!". Обычно проблема в этой строке. Если заменить 2 на 1, будет создана только одна текстура, а вторая будет в виде белой текстуры. Если заменить 2 на 3, то программа может зависнуть!
Вы должны вызывать glGenTextures() один раз. После вызова glGenTexture, необходимо сгенерировать все ваши текстуры. Я видел людей, которые вставляют вызов glGenTextures() перед созданием каждой текстуры. Обычно они ссылаются на то, что новая текстура перезаписывает все уже созданные текстуры. Было бы неплохо, сначала решить, сколько текстур необходимо сделать, а затем вызвать один раз glGenTextures(), а потом создать все текстуры. Не хорошо помещать вызов glGenTextures() в цикл без причины.
glGenTextures(2, &texture[0]); // Создание 2-х текстур
for (loop=0; loop<2; loop++) // Цикл для всех текстур
{
// Создание всех текстур
glBindTexture(GL_TEXTURE_2D, texture[loop]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, 3,
TextureImage[loop]->sizeX, TextureImage[loop]->sizeY, 0,
GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop]->data);
}
}
Следующие стоки кода проверяют, занимает ли загруженное нами rgb изображение для создания текстуры память. Если да, то высвобождаем ее. Заметьте, мы проверяем и освобождаем обе записи для изображений. Если мы используем три различные изображения для текстур, то необходимо проверить и освободить память из-под 3-х изображений.
for (loop=0; loop<2; loop++)
{
if (TextureImage[loop]) // Если текстура существует
{
if (TextureImage[loop]->data) // Если изображение текстуры существует
{
// Освобождаем память от изображения текстуры
free(TextureImage[loop]->data);
}
free(TextureImage[loop]); // Освобождаем память от структуры изображения
}
}
return Status; // Возвращаем статус
}
Теперь создадим сам наш шрифт. Я пройду эту секцию с особо детальным описанием. Это не очень сложно, но там есть немного математики, которую нужно понять, а я знаю, что математика не всем нравится.
GLvoid BuildFont(GLvoid) // Создаем список отображения нашего шрифта
{
Следующие две переменные будем использовать для сохранения позиции каждой буквы внутри текстуры шрифта. cx будет содержать позицию в текстуре по горизонтали, а cy содержать позицию по вертикали.
float cx; // Содержит X координату символа
float cy; // Содержит Y координату символа
Дальше мы скажем OpenGL, что хотим 256 списков отображения. Переменная base будет указывать на положение первого списка отображения. Второй будет base+1, третий base+2, и т.д. Вторая строка кода ниже, выбирает нашу текстуру шрифта (texture[0]).
base=glGenLists(256); // Создаем списки
glBindTexture(GL_TEXTURE_2D, texture[0]);// Выбираем текстуру шрифта
Дальше мы начнем наш цикл. В цикле создадим 256 символов, сохраняя каждый символ в своем собственном списке отображения.
for (loop=0; loop<256; loop++) // Цикл по всем 256 спискам
{
Первая строка ниже, может показаться загадочной. Символ % означает остаток от деления loop на 16. cx будет двигаться по текстуре шрифта слева направо. Позже вы заметите в коде, что мы вычитаем из 1 cy, чтобы двигаться сверху вниз, вместо того, чтобы двигаться снизу вверх. Символ % довольно трудно объяснить, но я попробую.
Все, о чем мы говорим, (loop % 16) /16.0f просто переводит результат в координаты текстуры. Поэтому, если loop было равно 16, cx будет равно остатку от деления 16 на 16, то есть 0. А cy равно 16/16, то есть 1. Поэтому мы двигаемся вниз на высоту одного символа, и совсем не двигаемся вправо. Теперь, если loop равно 17, cx будет равно 17/16, что равно 1.0625. Остаток .0625 или 1/16-ая. Это значит, что мы двигаемся на один символ вправо. cy все еще будет равно 1, потому что нам важны только знаки слева от десятичной запятой. 18/16 даст нам 2/16, двигая на 2 символа вправо, и все еще на один вниз. Если loop равно 32, cx будет опять 0, потому что остатка от деления нет, когда делим 32 на 16, но cy равно 2. Потому что число слева от десятичной запятой будет 2, двигая нас на 2 символа вниз от самого верха текстуры шрифта. Не так ли?
cx=float(loop%16)/16.0f; // X координата текущего символа
cy=float(loop/16)/16.0f; // Y координата текущего символа
Вау! Ок. Итак, теперь мы построим наш 2D шрифт, выбирая каждый символ из текстуры шрифта, в зависимости от значений cx и cy. В строках ниже мы добавим loop к значению base, если мы этого не сделаем, то каждая буква будет построена в первом дисплейном списке. Мы точно не хотим, чтобы это случилось, поэтому добавим loop к base и каждый следующий символ, который мы создаем, сохранится в следующем доступном списке отображения.
glNewList(base+loop,GL_COMPILE); // Начинаем делать список
Теперь, когда мы выбрали список отображения, который мы построили, мы создадим символ. Этого мы добъемся, создавая четырехугольник, и затем текстурируя его одним символом из текстуры шрифта.
glBegin(GL_QUADS); // Используем четырехугольник, для каждого символа
cx и cy будут содержать очень маленькие значения от 0.0f до 1.0f. Оба они будут равны 0 в первой строчке кода ниже, а именно: glTexCoord2f(0.0f,1-0.0f-0.0625f). Помните, что 0.0625 это 1/16-ая нашей текстуры, или ширина/высота одного символа. Координаты текстуры ниже будут координатами левой нижней точки текстуры.
Заметьте, мы используем glVertex2i(x, y) вместо glVertex3f(x, y, z). Наш шрифт – это двумерный шрифт, поэтому нам не нужна координата z. Поскольку мы используем плоский экран (Ortho screen – ортографическая или параллельная проекция), нам не надо сдвигаться вглубь экрана. Мы должны сделать, чтобы нарисовать на плоском экране, это задать x и y координаты. Так как наш экран в пикселах от 0 до 639 и от 0 до 479, нам вообще не надо использовать плавающую точку или отрицательные значения :). Используя плоский экран, мы получаем (0, 0) в нижнем левом углу. (640, 480) будет в верхнем правом углу. 0 - левый край по оси x, 639 - правый край экрана по оси x. 0 – верхний край экрана по оси y и 479 – нижний край экрана на оси y. Проще говоря, мы избавились от отрицательных координат. Это тоже удобно для тех, кто не заботится о перспективе и предпочитает работать с пикселами больше, чем с экранными единицами.
glTexCoord2f(cx,1-cy-0.0625f); // Точка в текстуре (Левая нижняя)
glVertex2i(0,0); // Координаты вершины (Левая нижняя)
Следующая точка на текстуре будет 1/16-ая правее предыдущей точки (точнее ширина одного символа). Поэтому это будет нижняя правая точка текстуры.
// Точка на текстуре (Правая нижняя)
glTexCoord2f(cx+0.0625f,1-cy-0.0625f);
glVertex2i(16,0); // Координаты вершины (Правая нижняя)
Третья точка текстуры лежит в дальнем правом конце символа, но сдвинута вверх на 1/16-ую текстуры (точнее на высоту одного символа). Это будет верхняя правая точка отдельного символа.
glTexCoord2f(cx+0.0625f,1-cy); // Точка текстуры (Верхняя правая)
glVertex2i(16,16); // Координаты вершины (Верхняя правая)
Наконец, мы двигаемся влево, чтобы задать нашу последнюю точку в верхнем левом углу символа.
glTexCoord2f(cx,1-cy); // Точка текстуры (Верхняя левая)
glVertex2i(0,16); // Координаты вершины (Верхняя левая)
glEnd(); // Конец построения четырехугольника (Символа)
Наконец, мы перемещаемся на 10 пикселей вправо, двигаясь вправо по текстуре. Если мы не передвинемся, символы будут рисоваться поверх друг друга. Так как наш шрифт очень узкий, мы не будем двигаться на 16 пикселов вправо. Если мы это сделаем, то будет слишком большой пропуск между каждым символом. Двигаясь на 10 пикселей, мы уменьшаем расстояние между символами.
glTranslated(10,0,0); // Двигаемся вправо от символа
glEndList(); // Заканчиваем создавать список отображения
} // Цикл для создания всех 256 символов
}
Следующая секция кода такая же как мы делали в предыдущих уроках, для освобождения списка отображения, перед выходом из программы. Все 256 экранных списков, начиная от base, будут удалены. (Это хорошо!)
GLvoid KillFont(GLvoid) // Удаляем шрифт из памяти
{
glDeleteLists(base,256); // Удаляем все 256 списков отображения
}
Следующая секция кода содержит все рисование. Все довольно ново, поэтому я постараюсь объяснить каждую строчку особенно детально. Одно маленькое замечание: можно добавить переменные для поддержки размеров, пропусков, и кучу проверок для восстановления настроек которые были до того, как мы решили их напечатать.
glPrint() имеет четыре параметра. Первый это координата x на экране (позиция слева на право). Следующая это y координата на экране (сверху вниз... 0 внизу, большие значения наверху). Затем нашу строку string (текст, который мы хотим напечатать), и, наконец, переменную set. Если посмотреть на картинку, которую сделал Giuseppe D'Agata, можно заметить, что там два разных набора символов. Первый набор - обычные символы, а второй набор - наклонные. Если set = 0, то выбран первый набор. Если set = 1 или больше, то выбран второй набор символов.
GLvoid glPrint(GLint x, GLint y, char *string, int set) // Где печатать
{
Первое, что мы сделаем - это проверим, что set от 0 до 1. Если set больше 1, то присвоим ей значение 1.
if (set>1) // Больше единицы?
{
set=1; // Сделаем Set равное единице
}
Теперь выберем нашу текстуру со шрифтом. Мы делаем это только, если раньше была выбрана другая текстура, до того как мы решили печатать что-то на экране.
glBindTexture(GL_TEXTURE_2D, texture[0]); // Выбираем нашу текстуру шрифта
Теперь отключим проверку глубины. Причина того, почему я так делаю, в том, что смешивание работает приятнее. Если не отменить проверку глубины, то текст может проходить за каким-нибудь объектом, или смешивание может выглядеть неправильно. Если вы не хотите смешивать текст на экране (из-за смешивания, т.е прозрачности черный фон вокруг символов не виден) можете оставить проверку глубины.
glDisable(GL_DEPTH_TEST); // Отмена проверки глубины
Следующее несколько строк очень важны! Мы выбираем нашу матрицу проекции. Прямо после этого мы используем команду glPushMatrix(). glPushMatrix сохраняет текущую матрицу проекции. Похоже на кнопку "память" на калькуляторе.
glMatrixMode(GL_PROJECTION); // Выбираем матрицу проекции
glPushMatrix(); // Сохраняем матрицу проекции
Теперь, когда наша матрица сохранена, мы сбрасываем ее и устанавливаем плоский экран. Первое и третье число (0) задают нижний левый угол экрана. Мы можем сделать левую сторону экрана -640, если захотим, но зачем нам работать с отрицательными числами, если это не нужно. Второе и четвертое число задают верхний правый угол экрана. Неплохо установить эти значения равными текущему разрешению. Глубины нет, поэтому устанавливаем значения z в -1 и 1.
glLoadIdentity(); // Сбрасываем матрицу проекции
glOrtho(0,640,0,480,-1,1); // Устанавливаем плоский экран
Теперь выбираем нашу матрицу просмотра модели и сохраняем текущие установки, используя glPushMatrix(). Далее сбрасываем матрицу просмотра модели, так что можно работать, используя ортографическую проекцию.
glMatrixMode(GL_MODELVIEW); // Выбираем матрицу модели просмотра
glPushMatrix(); // Сохраняем матрицу модели просмотра
glLoadIdentity(); // Сбрасываем матрицу модели просмотра
Сохранив настройки перспективы и установив плоский экран, можно рисовать текст. Начнем с перемещения в позицию на экране, где мы хотим нарисовать текст. Используем glTranslated() вместо glTranslatef(), так как мы работаем с пикселами, поэтому точки с дробными значениями не имеют смысла. В конце концов, нельзя использовать половину пиксела :).
glTranslated(x,y,0); // Позиция текста (0,0 - Нижняя левая)
Строка ниже выбирает, каким набором символов мы хотим воспользоваться. Если мы хотим использовать второй набор символов, то добавляем 128 к base (128 - половина 256 символов). Добавляя 128, мы пропускаем первые 128 символов.
glListBase(base-32+(128*set)); // Выбираем набор символов (0 или 1)
Сейчас, все что осталось - это нарисовать символы на экране. Делаем это так же как во всех других уроках со шрифтами. Используем glCallLists(). strlen(string) это длина строки (сколько символов мы хотим нарисовать), GL_BYTE означает то, что каждый символ представляется одним байтом (байт это любое значение от 0 до 255). Наконец, string содержит тот текст, который надо напечатать на экране.
glCallLists(strlen(string),GL_BYTE,string); // Рисуем текст на экране
Все, что надо теперь сделать, это восстановить перспективу. Мы выбираем матрицу проектирования и используем glPopMatrix(), чтобы восстановить установки, сохраненные с помощью glPushMatrix(). Важно восстановить их в обратном порядке, в том в котором мы их сохраняли.
glMatrixMode(GL_PROJECTION); // Выбираем матрицу проекции
glPopMatrix(); // Восстанавливаем старую матрицу проекции
Теперь мы выбираем матрицу просмотра модели и делаем то же самое. Мы используем glPopMatrix(), чтобы восстановить нашу матрицу просмотра модели, на ту, которая была, прежде чем мы устанавливали плоский экран.
glMatrixMode(GL_MODELVIEW); // Выбираем матрицу просмотра модели
glPopMatrix(); // Восстанавливаем старую матрицу проекции
Наконец, разрешаем проверку глубины. Если мы не запрещали проверку глубины в коде раньше, то нам не нужна эта строка.
glEnable(GL_DEPTH_TEST); // Разрешаем тест глубины
}
В ReSizeGLScene() ничего менять не надо, так что переходим к InitGL().
int InitGL(GLvoid) // Все установки для OpenGL здесь
{
Переходим к коду построения текстур. Если построить текстуры не удалось по какой-либо причине, то возвращаем значение FALSE. Это позволит нашей программе узнать, что произошла ошибка, и программа изящно завершится.
if (!LoadGLTextures()) // Переходим к загрузке текстуры
{
return FALSE; // Если текстура не загрузилась - возвращаем FALSE
}
Если ошибок не было, переходим к коду построения шрифта. Т.к. ничего не может случиться при построении шрифта, поэтому проверку ошибок не включаем.
BuildFont(); // Создаем шрифт
Теперь делаем обычную настройку GL. Мы установим черный цвет фона для очистки, зададим значение глубины в 1.0. Выбираем режим проверки глубины вместе со смешиванием. Мы разрешаем сглаженное заполнение и, наконец, разрешаем 2-мерное текстурирование.
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Очищаем фон черным цветом
glClearDepth(1.0); // Очистка и сброс буфера глубины
glDepthFunc(GL_LEQUAL); // Тип теста глубины
glBlendFunc(GL_SRC_ALPHA,GL_ONE); // Выбор типа смешивания
glShadeModel(GL_SMOOTH); // Сглаженное заполнение
glEnable(GL_TEXTURE_2D); // 2-мерное текстурирование
return TRUE; // Инициализация прошла успешно
}
Следующая секция кода создает сцену. Мы рисуем сначала 3-х мерный объект и потом текст, поэтому текст будет поверх 3-х мерного объекта, вместо того, чтобы объект закрывал текст сверху. Причина, по которой я добавил 3-х мерный объект, заключается в том, чтобы показать, что перспективная и ортографическая проекции могут быть использованы одновременно.
int DrawGLScene(GLvoid) // Здесь мы рисуем все объекты
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экрана и буфера глубины
glLoadIdentity(); // Сброс матрицы просмотра модели
Мы выбрали нашу bumps.bmp текстуру, и сейчас можно построить маленький 3-х мерный объект. Мы сдвигаемся на 5 экранных единиц вглубь экрана, так чтобы можно было увидеть объект. Мы вращаем объект на 45 градусов вдоль оси z. Это повернет наш четырехугольник на 45 градусов по часовой стрелке, и он будет больше похож на ромб, чем на прямоугольник.
glBindTexture(GL_TEXTURE_2D, texture[1]);// Выбираем вторую текстуру
glTranslatef(0.0f,0.0f,-5.0f); // Сдвигаемся на 5 единиц вглубь экрана
glRotatef(45.0f,0.0f,0.0f,1.0f); // Поворачиваем на 45 градусов (по часовой стрелке)
После поворота на 45 градусов, мы вращаем объект вокруг осей x и y, с помощью переменной cnt1*30. Это заставляет наш объект вращаться вокруг своей оси, подобно алмазу.
glRotatef(cnt1*30.0f,1.0f,1.0f,0.0f); // Вращение по X & Y на cnt1 (слева направо)
Отменяем смешивание (мы хотим, чтобы 3-х мерный объект был сплошным), и устанавливаем цвет на ярко белый. Затем рисуем один текстурированный четырехугольник.
glDisable(GL_BLEND); // Отменяем смешивание перед рисованием 3D
glColor3f(1.0f,1.0f,1.0f); // Ярко белый
glBegin(GL_QUADS); // Рисуем первый текстурированный прямоугольник
glTexCoord2d(0.0f,0.0f); // Первая точка на текстуре
glVertex2f(-1.0f, 1.0f); // Первая вершина
glTexCoord2d(1.0f,0.0f); // Вторая точка на текстуре
glVertex2f( 1.0f, 1.0f); // Вторая вершина
glTexCoord2d(1.0f,1.0f); // Третья точка на текстуре
glVertex2f( 1.0f,-1.0f); // Третья вершина
glTexCoord2d(0.0f,1.0f); // Четвертая точка на текстуре
glVertex2f(-1.0f,-1.0f); // Четвертая вершина
glEnd(); // Заканчиваем рисование четырехугольника
Сразу, после того как мы нарисовали первый четырехугольник, мы поворачиваемся на 90 градусов по осям x и y. Затем рисуем другой четырехугольник. Второй четырехугольник проходит сквозь середину первого, в результате получается красивая фигура.
// Поворачиваемся по X и Y на 90 градусов (слева на право)
glRotatef(90.0f,1.0f,1.0f,0.0f);
glBegin(GL_QUADS); // Рисуем второй текстурированный четырехугольник
glTexCoord2d(0.0f,0.0f); // Первая точка на текстуре
glVertex2f(-1.0f, 1.0f); // Первая вершина
glTexCoord2d(1.0f,0.0f); // Вторая точка на текстуре
glVertex2f( 1.0f, 1.0f); // Вторая вершина
glTexCoord2d(1.0f,1.0f); // Третья точка на текстуре
glVertex2f( 1.0f,-1.0f); // Третья вершина
glTexCoord2d(0.0f,1.0f); // Четвертая точка на текстуре
glVertex2f(-1.0f,-1.0f); // Четвертая вершина
glEnd(); // Заканчиваем рисовать четырехугольник
После того как нарисованы четырехугольники, разрешаем смешивание и рисуем текст.
glEnable(GL_BLEND); // Разрешаем смешивание
glLoadIdentity(); // Сбрасываем просмотр
Мы используем такой же код для раскрашивания, как в предыдущих примерах с текстом. Цвет меняется постепенно по мере движения текста по экрану.
// Изменение цвета основывается на положении текста
glColor3f(1.0f*float(cos(cnt1)),1.0f*float(sin(cnt2)),
1.0f-0.5f*float(cos(cnt1+cnt2)));
Затем мы рисуем текст. Мы все еще используем glPrint(). Первый параметр - это координата x. Второй - координата y. Третий параметр ("NeHe") - текст, который надо написать на экране, и последний это набор символов (0 - обычный, 1 - наклонный).
Как вы могли заметить, мы двигаем текст по экрану используя SIN и COS, используя счетчики cnt1 и cnt2. Если вы не понимаете, что делают SIN и COS, вернитесь и прочитайте тексты предыдущих уроков.
// Печатаем GL текст на экране
glPrint(int((280+250*cos(cnt1))),int(235+200*sin(cnt2)),"NeHe",0);
glColor3f(1.0f*float(sin(cnt2)),
1.0f-0.5f*float(cos(cnt1+cnt2)),1.0f*float(cos(cnt1)));
// Печатаем GL текст на экране
glPrint(int((280+230*cos(cnt2))),int(235+200*sin(cnt1)),"OpenGL",1);
Мы устанавливаем темно синий цвет и пишем имя автора внизу экрана. Затем мы пишем его имя на экране опять, используя ярко белые символы.
glColor3f(0.0f,0.0f,1.0f); // Устанавливаем синий цвет
glPrint(int(240+200*cos((cnt2+cnt1)/5)),
2,"Giuseppe D'Agata",0); // Рисуем текст на экране
glColor3f(1.0f,1.0f,1.0f); // Устанавливаем белый цвет
glPrint(int(242+200*cos((cnt2+cnt1)/5)),
2,"Giuseppe D'Agata",0); // Рисуем смещенный текст
Последнее, что мы сделаем - это добавим обоим счетчикам разные значения. Это заставит текст двигаться и вращаться как 3-х мерный объект.
cnt1+=0.01f; // Увеличим первый счетчик
cnt2+=0.0081f; // Увеличим второй счетчик
return TRUE; // Все прошло успешно
}
Код в KillGLWindow(), CreateGLWindow() и WndProc() не изменился, поэтому пропустим его.
int WINAPI WinMain( HINSTANCE hInstance, // Экземпляр
HINSTANCE hPrevInstance, // Предыдущий экземпляр
LPSTR lpCmdLine, // Параметры командной строки
int nCmdShow) // Стиль вывода окна
{
MSG msg; // Структура сообщения
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 & Giuseppe D'Agata's 2D Font Tutorial",640,480,16,fullscreen))
{
return 0; // Окно не создалось - выходим
}
while(!done) // Цикл пока done=FALSE
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Пришло сообщение?
{
if (msg.message==WM_QUIT) // Это сообщение о выходе?
{
done=TRUE; // Если да, то done=TRUE
}
else // Если нет, то обрабатываем сообщение
{
TranslateMessage(&msg);// Переводим сообщение
DispatchMessage(&msg); // Отсылаем сообщение
}
}
else // Нет сообщений
{
// Рисуем сцену. Ждем клавишу ESC или сообщение о выходе из DrawGLScene()
// Активно? Было сообщение о выходе?
if ((active && !DrawGLScene()) || keys[VK_ESCAPE])
{
done=TRUE; // ESC или DrawGLScene сообщает о выходе
}
else // Не время выходить, обновляем экран
{
SwapBuffers(hDC); // Меняем экраны (Двойная буферизация)
}
}
}
// Закрываем приложение
Последнее, что надо сделать, это добавить KillFont() в конец KillGLWindow(), как я показывал раньше. Важно добавить эту строчку. Она очищает память перед выходом из программы.
if (!UnregisterClass("OpenGL",hInstance)) // Можем удалить регистрацию класса
{
MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",
MB_OK | MB_ICONINFORMATION);
hInstance=NULL; // Устанавливаем hInstance в NULL
}
KillFont(); // Уничтожаем шрифт
}
Я думаю, что могу официально заявить, что моя страничка теперь учит всем возможным способам, как писать текст на экране {усмешка}. В целом, я думаю это хороший урок. Код можно использовать на любом компьютере, на котором работает OpenGL, его легко использовать, и писать с помощью него текст на экране довольно просто.
Я бы хотел поблагодарить Giuseppe D'Agata за оригинальную версию этого урока. Я сильно его изменил и преобразовал в новый базовый код, но без его присланного кода, я, наверное, не смог бы написать этот урок. Его версия кода имела побольше опций, таких как пробелы между символами и т.д., но я дополнил его прикольным 3-х мерным объектом {усмешка}.
Я надеюсь, всем понравился этот урок. Если у вас есть вопросы, пишите Giuseppe D'Agata или мне.
по OpenGL. Квадратирование
Quadratics
Квадратирование (quadratic) - это способ отображения сложных объектов, обычно для рисования которых, нужно несколько циклов FOR и некоторые основы тригонометрии. (Прим. переводчика: квадратирование - представление сложных объектов с использованием четырехугольников).
Мы будем использовать код 7-ого урока. Мы добавим 7 переменных и изменим текстуру для разнообразия.
#include <windows.h> // Заголовочный файл для Windows
#include <stdio.h> // Заголовочный файл для стандартной библиотеки ввода/вывода
#include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32
#include <gl\glu.h> // Заголовочный файл для библиотеки GLu32
#include <gl\glaux.h> // Заголовочный файл для библиотеки GLaux
HDC hDC=NULL; // Приватный контекст устройства GDI
HGLRC hRC=NULL; // Постоянный контекст рендеринга
HWND hWnd=NULL; // Сохраняет дескриптор окна
HINSTANCE hInstance; // Сохраняет экземпляр приложения
bool keys[256]; // Массив для работы с клавиатурой
bool active=TRUE; // Флаг активации окна, по умолчанию = TRUE
bool fullscreen=TRUE; // Флаг полноэкранного вывода
bool light; // Освещение Вкл/Выкл
bool lp; // L нажата?
bool fp; // F нажата?
bool sp; // Пробел нажат? ( НОВОЕ )
int part1; // Начало диска ( НОВОЕ )
int part2; // Конец диска ( НОВОЕ )
int p1=0; // Приращение 1 ( НОВОЕ )
int p2=1; // Приращение 2 ( НОВОЕ )
GLfloat xrot; // X вращение
GLfloat yrot; // Y вращение
GLfloat xspeed; // X скорость вращения
GLfloat yspeed; // Y скорость вращения
GLfloat z=-5.0f; // Глубина экрана
GLUquadricObj *quadratic; // Место для хранения объекта Quadratic ( НОВОЕ )
GLfloat LightAmbient[]= { 0.5f, 0.5f, 0.5f, 1.0f }; // Фоновое значение света
GLfloat LightDiffuse[]= { 1.0f, 1.0f, 1.0f, 1.0f }; // Значение рассеянного света
GLfloat LightPosition[]= { 0.0f, 0.0f, 2.0f, 1.0f }; // Позиция источника
GLuint filter; // Какой фильтр использовать
GLuint texture[3]; // Место для 3-х текстур
GLuint object=0; // Какой объект рисовать ( НОВОЕ )
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление WndProc
Ок. Теперь обратимся к InitGL(). Мы собираемся добавить 3 строчки кода, для инициализации нашего квадратичного объекта. Добавьте эти 3 строки после инициализации освещения (light1), но до строки return true. Первая строка инициализирует квадратичный объект и создает указатель на то место в памяти, где он будет содержаться. Если он не может быть создан, то будет возвращен 0. Вторая строка кода создает плавные нормали на квадратичном объекте, поэтому освещение будет выглядеть хорошо. Другое возможное значение - GL_NONE и GL_FLAT. Наконец, мы включим текстурирование на нашем квадратичном объекте.
quadratic=gluNewQuadric(); // Создаем указатель на квадратичный объект ( НОВОЕ )
gluQuadricNormals(quadratic, GLU_SMOOTH); // Создаем плавные нормали ( НОВОЕ )
gluQuadricTexture(quadratic, GL_TRUE); // Создаем координаты текстуры ( НОВОЕ )
Теперь я решил оставить куб в этом уроке, так, чтобы вы смогли увидеть, как текстура отображается на квадратичном объекте. Я решил поместить куб в отдельную функцию, поэтому, когда мы напишем функцию рисования, она станет намного проще. Все узнают этот код.
GLvoid glDrawCube() // Рисование куба
{
glBegin(GL_QUADS); // Начинаем рисовать четырехугольники
// Передняя сторона
glNormal3f( 0.0f, 0.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); // Верх Право на текстуре и четырехугольнике
glTexCoord2f(0.0f, 1.0f);
glVertex3f(-1.0f, 1.0f, 1.0f); // Верх Лево на текстуре и четырехугольнике
// Задняя сторона
glNormal3f( 0.0f, 0.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); // Верх Лево на текстуре и четырехугольнике
glTexCoord2f(0.0f, 0.0f);
glVertex3f( 1.0f, -1.0f, -1.0f); // Низ Лево на текстуре и четырехугольнике
// Верхняя грань
glNormal3f( 0.0f, 1.0f, 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,-1.0f, 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( 1.0f, 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(-1.0f, 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(); // Заканчиваем рисование четырехугольника
}
Следующая функция - DrawGLScene. Я просто только написал case оператор для рисования разных объектов. Так же я использовал статическую переменную (локальная переменная, которая сохраняет свое значение каждый раз при вызове) для крутого эффекта, когда рисуем часть диска. Я собираюсь переписать всю функцию DrawGLScene для ясности.
Заметьте, что когда я говорю о параметрах, которые используются, я пропускаю первый параметр (quadratic). Этот параметр используется для всех объектов, которые мы рисуем, за исключением куба, поэтому я его пропускаю, когда говорю о параметрах.
int DrawGLScene(GLvoid) // Здесь мы все рисуем
{
// Очистка видео буфера и буфера глубины
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity(); // Сбрасываем вид
glTranslatef(0.0f,0.0f,z); // Перемещаемся вглубь экрана
glRotatef(xrot,1.0f,0.0f,0.0f); // Вращение по оси X
glRotatef(yrot,0.0f,1.0f,0.0f); // Вращение по оси Y
glBindTexture(GL_TEXTURE_2D, texture[filter]); // Выбираем фильтрацию текстуре
// Эта секция кода новая ( НОВОЕ )
switch(object) // Проверяем, какой объект рисовать
{
case 0: // Рисуем первый объект
glDrawCube(); // Рисуем наш куб
break; // Закончили
Второй объект, который мы создадим, будет цилиндр. Первый параметр (1.0f) – радиус основания цилиндра (низ). Второй параметр (1.0f) - это радиус цилиндра сверху. Третий параметр (3.0f) - это высота цилиндра (какой он длины). Четвертый параметр (32) – это сколько делений будет "вокруг" оси Z, и, наконец, пятый (32) - количество делений "вдоль" оси Z. Большее количество делений приведет к увеличению детализации объекта. Увеличивая количество делений, вы добавляете больше полигонов в объект. В итоге вы должны будем пожертвовать скоростью ради качества. Самое сложное - найти золотую середину.
case 1: // Рисуем второй объект
glTranslatef(0.0f,0.0f,-1.5f); // Центр цилиндра
gluCylinder(quadratic,1.0f,1.0f,3.0f,32,32); // Рисуем наш цилиндр
break; // Закончили
Третий объект, который мы создадим, будет поверхность в виде CD диска. Первый параметр (0.5f) - внутренний радиус цилиндра. Его значение может быть нулевым, что будет означать, что внутри нет отверстия. Чем больше будет внутренний радиус - тем больше будет отверстие внутри диска. Второй параметр (1.5f) - внешний радиус. Это значение должно будь больше, чем внутренний радиус. Если сделать его значение чуть больше чем внутренний радиус, то получится тонкое кольцо. Если это значение будет намного больше, чем внутренний радиус, то получится толстое кольцо. Третий параметр (32) – количество кусочков, из которых состоит диск. Думайте об этих кусочках, как о частях пиццы. Чем больше кусочков, тем глаже будет внешняя сторона диска. И, наконец, четвертый параметр (32) - это число колец, которые составляют диск. Кольца похожи на треки на записи. Круги внутри кругов. Эти кольца делят диск со стороны внутреннего радиуса к внешнему радиусу, улучшая детализацию. Опять же, чем больше делений, тем медленнее это будет работать.
case 2: // Рисуем третий объект
gluDisk(quadratic,0.5f,1.5f,32,32); // Рисуем диск (в виде CD)
break; // Закончили
Наш четвертый объект - объект, о котором я знаю то, что многие умерли, создавая его. Это сфера! Создать ее очень просто. Первый параметр - это радиус сферы. Если вы не очень знакомы с понятием радиус/диаметр и т.д., объясняю, радиус - это расстояние от центра объекта, до внешней стороны объекта. В нашем случае радиус равен 1.3f. Дальше идет количество разбиений "вокруг" оси Z (32), и количество разбиений "вдоль" оси Z (32). Большее количество придаст сфере большую гладкость. Для того, чтобы сфера была достаточно гладкой, обычно необходимо большое количество разбиений.
case 3: // Рисуем четвертый объект
gluSphere(quadratic,1.3f,32,32); // Рисуем сферу
break; // Закончили
Чтобы создать наш пятый объект мы воспользуемся той же командой, что и для цилиндра. Если вы помните, когда мы создавали цилиндр, первые два параметра контролировали радиусы цилиндра сверху и снизу. Для того, чтобы сделать конус, имеет смысл сделать один из радиусов равный нулю. Это создаст точку на конце. Итак, в коде ниже мы делаем радиус на верхней стороне цилиндра равным нулю. Это создаст нашу точку, которая и сделает наш конус.
case 4: // Рисуем пятый объект
glTranslatef(0.0f,0.0f,-1.5f); // Центр конуса
// Конус с нижним радиусом .5 и высотой 2
gluCylinder(quadratic,1.0f,0.0f,3.0f,32,32);
break; // Закончили
Наш шестой объект создан с помощью gluParticalDisc. Объект, который мы создадим этой командой точно такой же диск, который был до этого, но у команды gluParticalDisc есть еще 2 новых параметра. Пятый параметр (part1) - это угол, с которого мы хотим начать рисование диска. Шестой параметр - это конечный угол (или угол развертки). Это угол, который мы проходим от начального. Мы будем увеличивать этот угол, что позволит постепенно рисовать диск на экране, по направлению часовой стрелки. Как только конечный угол достигнет 360 градусов, мы начнем увеличивать начальный угол. Это будет выглядеть, как будто диск начал стираться, затем мы все начнем сначала!
case 5: // Рисуем шестой объект
part1+=p1; // Увеличиваем стартовый угол
part2+=p2; // Увеличиваем конечный угол
if(part1>359) // 360 градусов
{
p1=0; // Хватит увеличивать начальный угол
part1=0; // Устанавливаем начальный угол в 0
p2=1; // Начинаем увеличивать конечный угол
part2=0; // Начиная с 0
}
if(part2>359) // 360 градусов
{
p1=1; // Начинаем увеличивать начальный угол
p2=0; // Перестаем увеличивать конечный угол
}
// Диск, такой-же как в прошлый раз
gluPartialDisk(quadratic,0.5f,1.5f,32,32,part1,part2-part1);
break; // Закончили
};
xrot+=xspeed; // Увеличиваем угол поворота вокруг оси X
yrot+=yspeed; // Увеличиваем угол поворота вокруг оси Y
return TRUE; // Продолжаем
}
Теперь, в последней части, обработка клавиш. Просто добавим это, туда, где происходит проверка нажатия клавиш.
if (keys[' '] && !sp) // Нажата клавиша "пробел"?
{
sp=TRUE; // Если так, то устанавливаем sp в TRUE
object++; // Цикл по объектам
if(object>5) // Номер объекта больше 5?
object=0; // Если да, то устанавливаем 0
}
if (!keys[' ']) // Клавиша "пробел" отпущена?
{
sp=FALSE; // Если да, то устанавливаем sp в FALSE
}
Это все! Теперь вы можете рисовать квадратичные объекты в OpenGL. С помощью морфинга и квадратичных объектов можно сделать достаточно впечатляющие вещи. Анимированный диск - это пример простого морфинга.
Все у кого есть время зайдите на мой сайт, TipTup.Com 2000. (http://www.tiptup.com)
Машина моделирования частиц с использованием полосок из треугольников
Particle Engine Using Triangle Strips
Добро пожаловать на урок 19. Вы многое узнали, и теперь слегка развлечься. Я познакомлю Вас только с одной новой командой в этом уроке... Полоски из треугольников (triangle strip). Это очень удобно, и поможет ускорить ваши программы, когда надо рисовать множество треугольников.
В этом уроке я обучу Вас, как сделать несложную машину моделирования частиц (Particle Engine). Если Вы поймете, как работает машина моделирования частиц, Вы сможете создавать эффекты огня, дыма, водных фонтанов и так далее, не правда ли хорошее лакомство!
Я должен, однако предупредить Вас! На сегодняшний день я не написал ни одной машины моделирования частиц. Я знал, что 'знаменитая' машина моделирования частиц очень сложный кусок кода. Я делал попытки ранее, но обычно отказывался от этого после того, как я понимал, что я не смогу управлять всеми точками без того, чтобы не сойти с ума.
Вы можете мне не поверить, если я Вам скажу, что этот урок был написан на 100% с нуля. Я не заимствовал других идей, и я не имел никакой дополнительной технической информации. Я начал думать о частицах, и внезапно моя голова, наполнилась идеями (мозг включился?). Вместо того чтобы думать о каждой частице, как о пикселе, который был должен следовать от точки 'A' до точки 'B', и делать это или то, я решил, что будет лучше думать о каждой частице как об индивидуальном объекте, реагирующему на окружающую среду вокруг ее. Я дал каждой частице жизнь, случайное старение, цвет, скорость, гравитационное влияние и другое.
Вскоре я имел готовый проект. Я взглянул на часы, видимо инопланетяне снова забирали меня. Прошло 4 часа! Я помню, что время от времени пил кофе и закрывал глаза, но 4 часа...?
Поэтому, и хотя эта программа, по-моему, мнению грандиозная, и работает точно, так как я бы хотел, но возможно это не самый правильный способ создать машину моделирования частиц. Я не считаю это очень важным, так как машина моделирования частиц работает хорошо, и я могу использовать ее в моих проектах! Если Вы хотите знать, как точно это делается, то Вам надо потратить множество часов, просматривая сеть, в поисках подходящей информации. Только одно предупреждение. Те фрагменты кода, которые Вы найдете, могут оказаться очень загадочными :).
Этот урок использует код урока 1. Есть, однако, много нового кода, поэтому я будут переписывать любой раздел кода, который содержит изменения (это будет проще для понимания).
Используя код урока 1, мы добавим 5 новых строк кода в начало нашей программы. Первая строка (stdio.h) позволит нам читать данные из файлов. Такую же строку, мы добавили и к другим урокам, которые использовали текстуры. Во второй строке задается, сколько мы будем создавать частиц, и отображать на экране. MAX_PARTICLES будет равно любому значению, которое мы зададим. В нашем случае 1000. В третьей строке будет переключаться 'режим радуги' (включен или выключен). Мы установим по умолчанию включенным этот режим. sp и rp - переменные, которые мы будем использовать, чтобы предотвратить автогенерацию повторений нажатия клавиш пробел или ввод (enter), когда они нажаты.
#include <windows.h> // Заголовочный файл для Windows
#include <stdio.h> // Заголовочный файл для стандартной библиотеки ввода/вывода(НОВОЕ)
#include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32
#include <gl\glu.h> // Заголовочный файл для библиотеки GLu32
#include <gl\glaux.h> // Заголовочный файл для библиотеки GLaux
#define MAX_PARTICLES 1000 // Число частиц для создания ( НОВОЕ )
HDC hDC=NULL; // Приватный контекст устройства GDI
HGLRC hRC=NULL; // Постоянный контекст рендеринга
HWND hWnd=NULL; // Сохраняет дескриптор окна
HINSTANCE hInstance; // Сохраняет экземпляр приложения
bool keys[256]; // Массив для работы с клавиатурой
bool active=TRUE; // Флаг активации окна, по умолчанию = TRUE
bool fullscreen=TRUE;// Флаг полноэкранного режима
bool rainbow=true; // Режим радуги? ( НОВОЕ )
bool sp; // Пробел нажат? ( НОВОЕ )
bool rp; // Ввод нажат? ( НОВОЕ)
В следующих 4 строках - разнообразные переменные. Переменная slowdown (торможение) контролирует, как быстро перемещаются частицы. Чем больше ее значение, тем медленнее они двигаются. Чем меньше ее значение, тем быстрее они двигаются. Если значение задано маленькое, частицы будут двигаться слишком быстро! Скорость, с которой частиц перемещаются, будет задавать их траекторию движения по экрану. Более медленные частицы не будут улетать далеко. Запомните это.
Переменные xspeed и yspeed позволяют нам контролировать направлением хвоста потока частиц. xspeed будет добавляться к текущей скорости частицы по оси X. Если у xspeed - положительное значение, то наша частица будет смещаться направо. Если у xspeed - отрицательное значение, то наша частица будет смещаться налево. Чем выше значение, тем больше это смещение в соответствующем направлении. yspeed работает также, но по оси Y. Причина, по которой я говорю 'БОЛЬШЕ' в заданном направлении означает, что есть и другие коэффициенты, воздействующие на направление траектории частицы. xspeed и yspeed позволяют перемещать частицу в том направлении, в котором мы хотим.
Последняя переменная zoom. Мы используем эту переменную для панорамирования внутрь и вне нашей сцены. В машине моделирования частиц, это позволяет увеличить размер просмотра, или резко его сократить.
float slowdown=2.0f; // Торможение частиц
float xspeed; // Основная скорость по X (с клавиатуры изменяется направление хвоста)
float yspeed; // Основная скорость по Y (с клавиатуры изменяется направление хвоста)
float zoom=-40.0f; // Масштаб пучка частиц
Теперь мы задаем еще одну переменную названную loop. Мы будем использовать ее для задания частиц и вывода частиц на экран. col будет использоваться для сохранения цвета, с каким созданы частицы. delay будет использоваться, чтобы циклически повторять цвета в режиме радуги.
Наконец, мы резервируем память для одной текстуры (текстура частицы). Я решил использовать текстуру вместо точек по нескольким причинам. Наиболее важная причина, что точки замедляют быстродействие и выглядят очень плохо. Во-вторых, текстуры - более крутой способ :). Вы можете использовать квадратную частицу, крошечное изображение вашего лица, изображение звезды, и т.д. Больше возможностей!
GLuint loop; // Переменная цикла
GLuint col; // Текущий выбранный цвет
GLuint delay; // Задержка для эффекта радуги
GLuint texture[1]; // Память для нашей текстуры
Отлично, теперь интересный материал. В следующем разделе кода создается структура, которая описывает отдельную частицу. Это то место, где мы даем частице некоторые характеристики.
Мы начинаем с булевой переменной active. Если эта переменная ИСТИННА, то наша частица жива и летит. Если она равно ЛОЖЬ, то наша частица мертва, или мы выключили ее! В этой программе я не использую active, но ее удобно иметь на всякий случай (прим. переводчика: никогда неизвестно что будет потом, главное следовать определенным принципам).
Переменные life и fade управляют тем, как долго частица будет отображаться, и насколько яркой она будет, пока жива. Переменная life постепенно уменьшается на значение fade. В этой программе некоторые частицы будут гореть дольше, чем другие.
typedef struct // Структура частицы
{
bool active; // Активность (Да/нет)
float life; // Жизнь
float fade; // Скорость угасания
Переменные r, g и b задают красную, зеленую и синюю яркости нашей частицы. Чем ближе r к 1.0f, тем более красной будет частица. Если все 3 переменных равны 1.0f, то это создаст белую частицу.
float r; // Красное значение
float g; // Зеленное значение
float b; // Синие значение
Переменные x, y и z задают, где частица будет отображена на экране. x задает положение нашей частицы по оси X. y задает положение нашей частицы по оси Y, и, наконец, z задает положение нашей частицы по оси Z.
float x; // X позиция
float y; // Y позиция
float z; // Z позиция
Следующие три переменные важны. Эти три переменные управляют тем, как быстро частица перемещается по заданной оси, и в каком направлении двигается. Если xi имеет отрицательное значение, то наша частица будет двигаться влево. Если положительное, то вправо. Если yi имеет отрицательное значение, то наша частица будет двигаться вниз. Если положительное, то вверх. Наконец, если zi имеет отрицательное значение, то частица будет двигаться вглубь экрана, и, если положительное, то вперед к зрителю.
float xi; // X направление
float yi; // Y направление
float zi; // Z направление
Наконец, последние 3 переменные! О каждой из этих переменных можно думать как о гравитации. Если xg имеет положительное значение, то нашу частицу будет притягивать вправо. Если отрицательное, то нашу частицу будет притягивать влево. Поэтому, если наша частица перемещает влево (отрицательно) и мы применяем положительную гравитацию, то скорость в итоге замедлится настолько, что наша частица начнет перемещать в противоположном направлении. yg притягивает вверх или вниз, и zg притягивает вперед или назад от зрителя.
float xg; // X гравитация
float yg; // Y гравитация
float zg; // Z гравитация
particles - название нашей структуры.
}
particles; // Структура Частиц
Затем мы создаем массив называемый particle. Этот массив имеет размер MAX_PARTICLES. В переводе на русский язык: мы создаем память для хранения 1000 (MAX_PARTICLES) частиц. Это зарезервированная память будет хранить информацию о каждой индивидуальной частице.
particles particle[MAX_PARTICLES]; // Массив частиц (Место для информации о частицах)
Мы сокращаем код программы, при помощи запоминания наших 12 разных цветов в массиве цвета. Для каждого цвета от 0 до 11 мы запоминаем красную, зеленую, и, наконец, синею яркость. В таблице цветов ниже запомнено 12 различных цветов, которые постепенно изменяются от красного до фиолетового цвета.
static GLfloat colors[12][3]= // Цветовая радуга
{
{1.0f,0.5f,0.5f},{1.0f,0.75f,0.5f},{1.0f,1.0f,0.5f},{0.75f,1.0f,0.5f},
{0.5f,1.0f,0.5f},{0.5f,1.0f,0.75f},{0.5f,1.0f,1.0f},{0.5f,0.75f,1.0f},
{0.5f,0.5f,1.0f},{0.75f,0.5f,1.0f},{1.0f,0.5f,1.0f},{1.0f,0.5f,0.75f}
};
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление WndProc
Код загрузки картинки не изменился.
AUX_RGBImageRec *LoadBMP(char *Filename) // Загрузка картинки
{
FILE *File=NULL; // Индекс файла
if (!Filename) // Проверка имени файла
{
return NULL; // Если нет вернем NULL
}
File=fopen(Filename,"r"); // Проверим существует ли файл
if (File) // Файл существует?
{
fclose(File); // Закрыть файл
return auxDIBImageLoad(Filename); // Загрузка картинки и вернем на нее указатель
}
return NULL; // Если загрузка не удалась вернем NULL
}
В этом разделе кода загружается картинка (вызов кода выше) и конвертирует его в текстуру. Status используется за тем, чтобы отследить, действительно ли текстура была загружена и создана.
int LoadGLTextures() // Загрузка картинки и конвертирование в текстуру
{
int Status=FALSE; // Индикатор состояния
AUX_RGBImageRec *TextureImage[1]; // Создать место для текстуры
memset(TextureImage,0,sizeof(void *)*1); // Установить указатель в NULL
Наша текстура загружается кодом, который будет загружать нашу картинку частицы и конвертировать ее в текстуру с линейным фильтром.
if (TextureImage[0]=LoadBMP("Data/Particle.bmp")) // Загрузка текстуры частицы
{
Status=TRUE; // Задать статус в TRUE
glGenTextures(1, &texture[0]); // Создать одну текстуру
glBindTexture(GL_TEXTURE_2D, texture[0]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY,
0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
}
if (TextureImage[0]) // Если текстура существует
{
if (TextureImage[0]->data) // Если изображение текстуры существует
{
free(TextureImage[0]->data); // Освобождение памяти изображения текстуры
}
free(TextureImage[0]); // Освобождение памяти под структуру
}
return Status; // Возвращаем статус
}
Единственное изменение, которое я сделал в коде изменения размера, было увеличение области просмотра. Вместо 100.0f, мы можем теперь рассматривать частицы на 200.0f единиц в глубине экрана.
// Изменение размеров и инициализация окна GL
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
{
if (height==0) // Предотвращение деления на ноль, если окно слишком мало
{
height=1; // Сделать высоту равной единице
}
//Сброс текущей области вывода и перспективных преобразований
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION); // Выбор матрицы проекций
glLoadIdentity(); // Сброс матрицы проекции
// Вычисление соотношения геометрических размеров для окна
gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,200.0f); // ( МОДИФИЦИРОВАНО )
glMatrixMode(GL_MODELVIEW); // Выбор матрицы просмотра модели
glLoadIdentity(); // Сброс матрицы просмотра модели
}
Если Вы используете код урока 1, замените его на код ниже. Я добавил этот код для загрузки нашей текстуры и включения смешивания для наших частиц.
int InitGL(GLvoid) // Все начальные настройки OpenGL здесь
{
if (!LoadGLTextures()) // Переход на процедуру загрузки текстуры
{
return FALSE; // Если текстура не загружена возвращаем FALSE
}
Мы разрешаем плавное затенение, очищаем фон черным цветом, запрещаем тест глубины, разрешаем смешивание и наложение текстуры. После разрешения наложения текстуры мы выбираем нашу текстуру частицы.
glShadeModel(GL_SMOOTH); // Разрешить плавное затенение
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Черный фон
glClearDepth(1.0f); // Установка буфера глубины
glDisable(GL_DEPTH_TEST); // Запрещение теста глубины
glEnable(GL_BLEND); // Разрешаем смешивание
glBlendFunc(GL_SRC_ALPHA,GL_ONE); // Тип смешивания
// Улучшенные вычисления перспективы
glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST);
glHint(GL_POINT_SMOOTH_HINT,GL_NICEST); // Улучшенные точечное смешение
glEnable(GL_TEXTURE_2D); // Разрешение наложения текстуры
glBindTexture(GL_TEXTURE_2D,texture[0]); // Выбор нашей текстуры
В коде ниже инициализируется каждая из частиц. Вначале мы активизируем каждую частицу. Если частица - не активна, то она не будет появляться на экране, независимо оттого, сколько жизни у нее осталось.
После того, как мы сделали частицу активной, мы даем ей жизнь. Я сомневаюсь, что тот способ, с помощью которого я задаю жизнь, и угасание частицы, это самый лучший способ, но повторюсь еще раз, что это отлично работает! Полная жизнь - 1.0f. Это также дает частице полную яркость.
for (loop=0;loop<MAX_PARTICLES;loop++) // Инициализация всех частиц
{
particle[loop].active=true; // Сделать все частицы активными
particle[loop].life=1.0f; // Сделать все частицы с полной жизнью
Мы задаем, как быстро частица угасает, при помощи присвоения fade случайного значения. Переменная life будет уменьшаться на значение fade, каждый раз, после того как частица будет отображена. Случайное значение будет от 0 до 99. Его мы делим его на 1000, поэтому мы получим очень маленькое значение с плавающей запятой. В завершении мы добавим 0.003 к конечному результату так, чтобы скорость угасания никогда не равнялось 0.
//Случайная скорость угасания
particle[loop].fade=float(rand()%100)/1000.0f+0.003f;
Теперь, когда наша частица активна, и мы дали ей жизнь, пришло время задать ей цвет. Вначале, мы хотим, чтобы все частицы были разным цветом. Поэтому я, делаю каждую частицу одним из 12 цветов, которые мы поместили в нашу таблицу цветов вначале этой программы. Математика проста. Мы берем нашу переменную loop и умножаем ее на число цветов в нашей таблице цвета, и делим на максимальное число частиц (MAX_PARTICLES). Это препятствует тому, что заключительное значение цвета будет больше, чем наш максимальное число цветов (12).
Вот пример: 900 * (12/900) =12. 1000 * (12/1000) =12, и т.д.
particle[loop].r=colors[loop*(12/MAX_PARTICLES)][0]; // Выбор красного цвета радуги
particle[loop].g=colors[loop*(12/MAX_PARTICLES)][1]; // Выбор зеленного цвета радуги
particle[loop].b=colors[loop*(12/MAX_PARTICLES)][2]; // Выбор синего цвета радуги
Теперь мы зададим направление, в котором каждая частица двигается, наряду со скоростью. Мы умножаем результат на 10.0f, чтобы создать впечатление взрыва, когда программа запускается.
Мы начинаем с положительного или отрицательного случайного значения. Это значение будет использоваться для перемещения частицы в случайном направлении со случайной скоростью.
particle[loop].xi=float((rand()%50)-26.0f)*10.0f; // Случайная скорость по оси X
particle[loop].yi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Y
particle[loop].zi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Z
Наконец, мы задаем величину гравитации, которая воздействует на каждую частицу. В отличие от реальной гравитации, под действием которой все предметы падают вниз, наша гравитация сможет смещать частицы вниз, влево, вправо, вперед или назад (прим. переводчика: скорее всего это электромагнитное поле, а не гравитация). Вначале мы зададим гравитацию в полсилы, которая притягивает вниз. Чтобы сделать это, мы устанавливаем xg в 0.0f. Т.е. нет перемещения влево или вправо по плоскости X. Мы устанавливаем yg в -0.8f. Это создает притяжение вниз в полсилы. Если значение положительное, то притяжение вверх. Мы не хотим, чтобы частицы притягивались к нам или от нас, поэтому мы установим zg в 0.0f.
particle[loop].xg=0.0f; // Зададим горизонтальное притяжение в ноль
particle[loop].yg=-0.8f; // Зададим вертикальное притяжение вниз
particle[loop].zg=0.0f; // зададим притяжение по оси Z в ноль
}
return TRUE; // Инициализация завершена OK
}
Теперь интересный материал. В следующем разделе кода мы выводим частицу, проверяем гравитацию, и т.д. Очень важно, чтобы Вы поняли, что происходит там, поэтому, пожалуйста, читайте тщательно :).
Мы сбрасываем матрицу просмотра модели только однажды. Мы позиционируем частицы, используя команду glVertex3f() вместо использования перемещения их, при таком способе вывода частиц мы не изменяем матрицу просмотра модели при выводе наших частиц.
int DrawGLScene(GLvoid) // Здесь мы все рисуем
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экрана и буфера глубины
glLoadIdentity(); // Сброс матрицы просмотра модели
Мы начинаем наш вывод с цикла. Этот цикл обновит каждую из наших частиц.
for (loop=0;loop<MAX_PARTICLES;loop++) // Цикл по всем частицам
{
Вначале мы проверим, активна ли наша частица. Если она не активна, то ее не надо модифицировать. В этой программе они активны всегда. Но в вашей программе, Вы сможете захотеть сделать некоторые частицы неактивными.
if (particle[loop].active) // Если частицы не активны
{
Следующие три переменные x, y и z - временные переменные, которые мы будем использовать, чтобы запомнить позицию частицы по x, y и z. Отмечу, что мы добавляем zoom к позиции по z, так как наша сцена смещена в экран на значение zoom. particle[loop].x - это наша позиция по x для любой частицы, которую мы выводим в цикле. particle[loop].y - это наша позиция по y для нашей частицы, и particle[loop].z - это наша позиция по z.
float x=particle[loop].x; // Захватим позицию X нашей частицы
float y=particle[loop].y; // Захватим позицию Н нашей частицы
float z=particle[loop].z+zoom; // Позиция частицы по Z + Zoom
Теперь, когда мы имеем позицию частицы, мы можем закрасить частицу. particle[loop].r - это красная яркость частицы, particle[loop].g – это зеленая яркость, и particle[loop].b – это синяя яркость. Напомню, что я использую жизнь частицы (life) для альфа значения. По мере того, как частица умирает, она становится все более и более прозрачной, пока она, в конечном счете, не исчезнет. Именно поэтому, жизнь частиц никогда не должна быть больше чем 1.0f. Если Вы хотите, чтобы частицы горели более долго, пробуйте уменьшить скорость угасания так, чтобы частица не так быстро исчезла.
// Вывод частицы, используя наши RGB значения, угасание частицы согласно её жизни
glColor4f(particle[loop].r,particle[loop].g,particle[loop].b,particle[loop].life);
Мы задали позицию частицы и цвет. Все, что мы должны теперь сделать - вывести нашу частицу. Вместо использования текстурированного четырехугольника, я решил использовать текстурированную полоску из треугольников, чтобы немного ускорить программу. Некоторые 3D платы могут выводить треугольники намного быстрее чем, они могут выводить четырехугольники. Некоторые 3D платы конвертируют четырехугольник в два треугольника за Вас, но некоторые платы этого не делают. Поэтому мы сделаем эту работу сами. Мы начинаемся с того, что сообщаем OpenGL, что мы хотим вывести полоску из треугольников.
glBegin(GL_TRIANGLE_STRIP); // Построение четырехугольника из треугольной полоски
Цитата непосредственно из красной книги (OpenGL Red Book): полоска из треугольников рисуется как ряд треугольников (трехсторонних полигонов) используя вершины V0, V1, V2, затем V2, V1, V3 (обратите внимание на порядок), затем V2, V3, V4, и так далее. Порядок должен гарантировать, что все треугольники будут выведены с той же самой ориентацией так, чтобы полоска могла правильно формировать часть поверхности. Соблюдение ориентации важно для некоторых операций, типа отсечения. Должны быть, по крайней мере, 3 точки, чтобы было что-то выведено.
Рисунок 1. Полоска из двух треугольников
Поэтому первый треугольник выведен, используя вершины 0, 1 и 2. Если Вы посмотрите на рисунок 1, Вы увидите, что точки вершин 0, 1 и 2 действительно составляют первый треугольник (верхняя правая, верхняя левая, нижняя правая). Второй треугольник выведен, используя вершины 2, 1 и 3. Снова, если Вы посмотрите на рисунок 1, вершины 2, 1 и 3 создают второй треугольник (нижняя правая, верхняя левая, нижняя правая). Заметьте, что оба треугольника выведены с тем же самым порядком обхода (против часовой стрелки). Я видел несколько сайтов, на которых заявлялось, что каждый второй треугольник должен быть в противоположном направлении. Это не так. OpenGL будет менять вершины, чтобы гарантировать, что все треугольники выведены тем же самым способом!
Есть две хорошие причины, для того чтобы использовать полоски из треугольников. Во-первых, после определения первых трех вершин начального треугольника, Вы должны только определять одну единственную точку для каждого другого дополнительного треугольника. Эта точка будет объединена с 2 предыдущим вершинами для создания треугольника. Во-вторых, сокращая количество данных, необходимых для создания треугольников ваша программа будет работать быстрее, и количество кода или данных, требуемых для вывода объекта резко сократиться.
Примечание: число треугольников, которые Вы видите на экране, будет равно числу вершин, которые Вы зададите минус 2. В коде ниже мы имеем 4 вершины, и мы видим два треугольника.
glTexCoord2d(1,1); glVertex3f(x+0.5f,y+0.5f,z); // Верхняя правая
glTexCoord2d(0,1); glVertex3f(x-0.5f,y+0.5f,z); // Верхняя левая
glTexCoord2d(1,0); glVertex3f(x+0.5f,y-0.5f,z); // Нижняя правая
glTexCoord2d(0,0); glVertex3f(x-0.5f,y-0.5f,z); // Нижняя левая
Наконец мы сообщаем OpenGL, что мы завершили вывод нашей полоски из треугольников.
glEnd(); // Завершение построения полоски треугольников
Теперь мы можем переместить частицу. Математически это может выглядеть несколько странно, но довольно просто. Сначала мы берем текущую позицию x частицы. Затем мы добавляем значение смещения частицы по x, деленной на slowdown/1000. Поэтому, если наша частица была в центре экрана на оси X (0), наша переменная смещения (xi) для оси X равна +10 (смещение вправо от нас) и slowdown было равно 1, мы сместимся направо на 10/(1*1000), или на 0.01f. Если мы увеличим slowdown на 2, мы сместимся только на 0.005f. Буду надеяться, что это поможет Вам понять, как работает замедление (slowdown).
Это также объясняет, почему умножение начальных значений на 10.0f заставляет пиксели перемещаться намного быстрее, создавая эффект взрыва.
Мы используем ту же самую формулу для осей y и z, для того чтобы переместить частицу по экрану.
// Передвижение по оси X на скорость по X
particle[loop].x+=particle[loop].xi/(slowdown*1000);
// Передвижение по оси Y на скорость по Y
particle[loop].y+=particle[loop].yi/(slowdown*1000);
// Передвижение по оси Z на скорость по Z
particle[loop].z+=particle[loop].zi/(slowdown*1000);
После того, как мы вычислили перемещение частицы, следующее, что мы должны сделать, это учесть гравитацию или сопротивление. В первой строке ниже, мы делаем это, при помощи добавления нашего сопротивления (xg) к скорости перемещения (xi).
Предположим, что скорость перемещения равна 10, а сопротивление равно 1. Первый раз, когда частица выводиться на экран, сопротивление воздействует на нее. Во второй раз, когда она выводится, сопротивление будет действовать, и скорость перемещения понизится от 10 до 9. Это заставит частицу немного замедлится. В третий раз, когда частица выводиться, сопротивление действует снова, и скорость перемещения понизится до 8. Если бы частица горела больше чем 10 перерисовок, то она будет в итоге перемещаться в противоположном направлении, потому что скорость перемещения станет отрицательным значением.
Сопротивление применяется к скорости перемещения по y и z, так же, как и по x.
particle[loop].xi+=particle[loop].xg; // Притяжение по X для этой записи
particle[loop].yi+=particle[loop].yg; // Притяжение по Y для этой записи
particle[loop].zi+=particle[loop].zg; // Притяжение по Z для этой записи
В следующей строке забирается часть жизни от частицы. Если бы мы не делали этого, то частица бы никогда не сгорела бы. Мы берем текущую жизнь частицы и вычитаем значение угасания для этой частицы. Каждая частица имеет свое значение угасания, поэтому они будут гореть с различными скоростями.
particle[loop].life-=particle[loop].fade; // Уменьшить жизнь частицы на ‘угасание’
Теперь мы проверим, жива ли частица, после того как мы изменили ее жизнь.
if (particle[loop].life<0.0f) // Если частица погасла
{
Если частица мертва (сгорела), мы оживим ее. Мы сделаем это, задав ей полную жизнь и новую скорость угасания.
particle[loop].life=1.0f; // Дать новую жизнь
// Случайное значение угасания
particle[loop].fade=float(rand()%100)/1000.0f+0.003f;
Мы также сделаем сброс позиций частицы в центр экрана. Мы делаем это, при помощи сброса позиций x, y и z частицы в ноль.
particle[loop].x=0.0f; // На центр оси X
particle[loop].y=0.0f; // На центр оси Y
particle[loop].z=0.0f; // На центр оси Z
После того, как частица была сброшена в центр экрана, мы задаем ей новую скорость перемещения / направления. Отмечу, что я увеличил максимальную и минимальную скорость, с которой частица может двигаться со случайного значения в диапазоне 50 до диапазона 60, но на этот раз, мы не собирается умножать скорость перемещения на 10. Мы не хотим взрыва на этот раз, мы хотим иметь более медленно перемещающиеся частицы.
Также заметьте, что я добавил xspeed к скорости перемещения по оси X, и yspeed к скорости перемещения по оси Y. Это позволит нам позже контролировать, в каком направлении двигаются частицы.
particle[loop].xi=xspeed+float((rand()%60)-32.0f);//Скорость и направление по оси X
particle[loop].yi=yspeed+float((rand()%60)-30.0f);//Скорость и направление по оси Y
particle[loop].zi=float((rand()%60)-30.0f); //Скорость и направление по оси Z
Наконец мы назначаем частице новый цвет. В переменной col содержится число от 0 до 11 (12 цветов). Мы используем эту переменную для извлечения красной, зеленой и синей яркостей из нашей таблицы цветов, которую мы сделали в начале программы. В первой строке ниже задается красная яркость (r) согласно значению красного, сохраненного в colors[col][0]. Поэтому, если бы col равен 0, красная яркость равна 1.0f. Зеленые и синие значения получаются таким же способом.
Если Вы не поняли, как я получил значение 1.0f для красной яркости, если col - 0, я объясню это немного более подробно. Смотрите в начало программы. Найдите строку: static GLfloat colors[12][3]. Запомните, что есть 12 групп по 3 числа. Первые три числа - красная яркость. Второе значение - зеленая яркость, и третье значение - синяя яркость. [0], [1] и [2] ниже являются 1-ым, 2-ым и 3-ьим значениями, которые я только что упомянул. Если col равен 0, то мы хотим взглянуть на первую группу. 11 – последняя группа (12-ый цвет).
particle[loop].r=colors[col][0]; // Выбор красного из таблицы цветов
particle[loop].g=colors[col][1]; // Выбор зеленого из таблицы цветов
particle[loop].b=colors[col][2]; // Выбор синего из таблицы цветов
}
Строка ниже контролирует, насколько гравитация будет притягивать вверх. При помощи нажатия клавиши 8 на цифровой клавиатуре, мы увеличиваем переменную yg (y гравитация). Это вызовет притяжение вверх. Этот код расположен здесь, потому что это сделает нашу жизнь проще, гравитация будет назначена ко всем нашим частицам с помощью цикла. Если бы этот код был бы вне цикла, мы должны были бы создать другой цикл, чтобы проделать ту же самую работу, поэтому мы можем также сделать это прямо здесь.
// Если клавиша 8 на цифровой клавиатуре нажата и гравитация меньше чем 1.5
// тогда увеличим притяжение вверх
if (keys[VK_NUMPAD8] && (particle[loop].yg<1.5f)) particle[loop].yg+=0.01f;
Эта строка создает точно противоположный эффект. При помощи нажатия 2 на цифровой клавиатуре мы уменьшаем yg, создавая более сильное притяжение вниз.
// Если клавиша 2 на цифровой клавиатуре нажата и гравитация больше чем -1.5
// тогда увеличим притяжение вниз
if (keys[VK_NUMPAD2] && (particle[loop].yg>-1.5f)) particle[loop].yg-=0.01f;
Теперь мы модифицируем притяжение вправо. Если клавиша 6 на цифровой клавиатуре нажата, то мы увеличиваем притяжение вправо.
// Если клавиша 6 на цифровой клавиатуре нажата и гравитация меньше чем 1.5
// тогда увеличим притяжение вправо
if (keys[VK_NUMPAD6] && (particle[loop].xg<1.5f)) particle[loop].xg+=0.01f;
Наконец, если клавиша 4 на цифровой клавиатуре нажата, то наша частица будет больше притягиваться влево. Эти клавиши позволяют получить некоторые действительно интересные результаты. Например, Вы сможете сделать поток частиц, стреляющих прямо в воздух. Добавляя немного притяжения вниз, Вы сможете превратить поток частиц в фонтан воды!
// Если клавиша 4 на цифровой клавиатуре нажата и гравитация больше чем -1.5
// тогда увеличим притяжение влево
if (keys[VK_NUMPAD4] && (particle[loop].xg>-1.5f)) particle[loop].xg-=0.01f;
Я добавил этот небольшой код только для развлечения. Мой брат думает, что взрыв интересный эффект :). При помощи нажатия клавиши табуляции все частицы будут отброшены назад к центру экрана. Скорость перемещения частиц будет еще раз умножена на 10, создавая большой взрыв частиц. После того, как частицы взрыва постепенно исчезнут, появиться предыдущий столб частиц.
if (keys[VK_TAB]) // Клавиша табуляции вызывает взрыв
{
particle[loop].x=0.0f; // Центр по оси X
particle[loop].y=0.0f; // Центр по оси Y
particle[loop].z=0.0f; // Центр по оси Z
particle[loop].xi=float((rand()%50)-26.0f)*10.0f; // Случайная скорость по оси X
particle[loop].yi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Y
particle[loop].zi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Z
}
}
}
return TRUE; // Все OK
}
Код в KillGLWindow(), CreateGLWindow() и WndProc() не изменился, поэтому мы перейдем к WinMain(). Я повторю весь этот раздел кода, чтобы сделать просмотр кода проще.
int WINAPI WinMain(
HINSTANCE hInstance, // Экземпляр
HINSTANCE hPrevInstance, // Предыдущий экземпляр
LPSTR lpCmdLine, // Параметры командной строки
int nCmdShow) // Показать состояние окна
{
MSG msg; // Структура сообщения окна
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 Particle Tutorial",640,480,16,fullscreen))
{
return 0; // Выходим если окно не было создано
}
Далее наше первое изменение в WinMain(). Я добавил код, который проверяет, в каком режиме пользователь решил запустить программу - в полноэкранном режиме или в окне. Если используется полноэкранный режим, я изменяю переменную slowdown на 1.0f вместо 2.0f. Вы можете опустить этот небольшой код, если Вы хотите. Я добавил этот код, чтобы ускорить полноэкранный режим на моем 3dfx (потому что при этом выполнение программы намного медленнее, чем в режиме окна по некоторым причинам).
if (fullscreen) // Полноэкранный режим ( ДОБАВЛЕНО )
{
slowdown=1.0f; // Скорость частиц (для 3dfx) ( ДОБАВЛЕНО )
}
while (!done) // Цикл, который продолжается пока done=FALSE
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Есть ожидаемое сообщение?
{
if (msg.message==WM_QUIT) // Мы получили сообщение о выходе?
{
done=TRUE; // Если так done=TRUE
}
else // Если нет, продолжаем работать с сообщениями окна
{
TranslateMessage(&msg); // Переводим сообщение
DispatchMessage(&msg); // Отсылаем сообщение
}
}
else // Если сообщений нет
{
// Рисуем сцену. Ожидаем нажатия кнопки ESC и сообщения о выходе от DrawGLScene()
// Активно? Было получено сообщение о выходе?
if ((active && !DrawGLScene()) || keys[VK_ESCAPE])
{
done=TRUE; // ESC или DrawGLScene просигналили "выход"
}
else // Не время выходить, обновляем экран
{
SwapBuffers(hDC); // Переключаем буферы (Двойная буфферизация)
Я немного попотел со следующим куском кода. Обычно я не включаю все в одну строку, и это делает просмотр кода немного яснее :).
В строке ниже проверяется, нажата ли клавиша ‘+’ на цифровой клавиатуре. Если она нажата, и slowdown больше чем 1.0f, то мы уменьшаем slowdown на 0.01f. Это заставит частицы двигаться быстрее. Вспомните, что я говорил выше о торможении и как оно воздействует на скорость, с которой частица перемещается.
if (keys[VK_ADD] && (slowdown>1.0f)) slowdown-=0.01f;//Скорость частицы увеличилась
В этой строке проверяется, нажата ли клавиша ‘-‘ на цифровой клавиатуре. Если она нажата, и slowdown меньше чем 4.0f, то мы увеличиваем slowdown. Это заставляет частицы двигаться медленнее. Я выставил предел в 4.0f, потому что я не хочу, чтобы они двигались очень медленно. Вы можете изменить минимальные и максимальные скорости, на какие Вы хотите :).
if (keys[VK_SUBTRACT] && (slowdown<4.0f)) slowdown+=0.01f; // Торможение частиц
В строке ниже проверяется, нажата ли клавиша PAGE UP. Если она нажата, то переменная zoom увеличивается. Это заставит частицы двигаться ближе к нам.
if (keys[VK_PRIOR]) zoom+=0.1f; // Крупный план
В этой строке создается противоположный эффект. Нажимая клавишу Page down, zoom уменьшиться, и сцена сместиться глубже в экран. Это позволит нам увидеть больше частиц на экране, но при этом частицы будут меньше.
if (keys[VK_NEXT]) zoom-=0.1f; // Мелкий план
В следующей секции кода происходит проверка, была ли нажата клавиша Enter. Если она нажата в первый раз, и она не удерживается уже некоторое время, мы позволим компьютеру узнать, что она нажата, устанавливая rp в true. Тем самым мы переключим режим радуги. Если радуга была true, она станет false. Если она была false, то станет true. В последней строке проверяется, была ли клавиша Enter отпущена. Если это так, то rp устанавливается в false, сообщая компьютеру, что клавиша больше не нажата.
if (keys[VK_RETURN] && !rp) // нажата клавиша Enter
{
rp=true; // Установка флага, что клавиша нажата
rainbow=!rainbow; // Переключение режима радуги в Вкл/Выкл
}
if (!keys[VK_RETURN]) rp=false; // Если клавиша Enter не нажата – сбросить флаг
Код ниже немного запутанный. В первой строке идет проверка, нажата ли клавиша пробела и не удерживается ли она. Тут же проверяется, включен ли режим радуги, и если так, то проверяется значение переменной delay больше чем 25. delay - счетчик, который используется для создания эффекта радуги. Если Вы меняете цвет каждый кадр, то все частицы будут иметь разный цвет. При помощи создания задержки, группа частиц останется с одним цветом, прежде чем цвет будет изменен на другой.
Если клавиша пробел была нажата, или радуга включена, и задержка больше чем 25, цвет будет изменен!
if ((keys[' '] && !sp) || (rainbow && (delay>25))) // Пробел или режим радуги
{
Строка ниже была добавлена, для того чтобы режим радуги был выключен, если клавиша пробел была нажата. Если бы мы не выключили режим радуги, цвета продолжили бы циклически повторяться, пока клавиша Enter не была бы нажата снова. Это сделано, для того чтобы можно было просмотреть все цвета, нажимая пробел вместо Enter.
if (keys[' ']) rainbow=false; // Если пробел нажат запрет режима радуги
Если клавиша пробел была нажата, или режим радуги включен, и задержка больше чем 25, мы позволим компьютеру узнать, что пробел было нажат, делая sp равной true. Затем мы зададим задержку равной 0, чтобы снова начать считать до 25. Наконец мы увеличим переменную col, чтобы цвет изменился на следующий цвет в таблице цветов.
sp=true; // Установка флага нам скажет, что пробел нажат
delay=0; // Сброс задержки циклической смены цветов радуги
col++; // Изменить цвет частицы
Если цвет больше чем 11, мы сбрасываем его обратно в ноль. Если бы мы не сбрасывали col в ноль, наша программа попробовала бы найти 13-ый цвет. А мы имеем только 12 цветов! Попытка получить информацию о цвете, который не существует, привела бы к краху нашей программы.
if (col>11) col=0; // Если цвет выше, то сбросить его
}
Наконец, если клавиша пробел больше не нажата, мы позволяем компьютеру узнать это, устанавливая переменную sp в false.
if (!keys[' ']) sp=false; // Если клавиша пробел не нажата, то сбросим флаг
Теперь внесем немного управления нашими частицами. Помните, что мы создали 2 переменные в начале нашей программы? Одна называлась xspeed, и вторая называлась yspeed. Также Вы помните, что после того как частица сгорит, мы давали ей новую скорость перемещения и добавляли новую скорость или к xspeed или к yspeed. Делая это, мы можем повлиять, в каком направлении частицы будут двигаться, когда они впервые созданы.
Например. Пусть частица имеет скорость перемещения 5 по оси X и 0 по оси Y. Если мы уменьшим xspeed до -10, то скорость перемещения будет равна -10 (xspeed) +5 (начальная скорость). Поэтому вместо перемещения с темпом 10 вправо, частица будет перемещаться с темпом -5 влево Понятно?
Так или иначе. В строке ниже проверяем, нажата ли стрелка "вверх". Если это так, то yspeed будет увеличено. Это заставит частицы двигаться вверх. Частицы будут двигаться вверх с максимальной скоростью не больше чем 200. Если бы они двигались быстрее этого значения, то это выглядело бы не очень хорошо.
//Если нажата клавиша вверх и скорость по Y меньше чем 200, то увеличим скорость
if (keys[VK_UP] && (yspeed<200)) yspeed+=1.0f;
В этой строке проверяем, нажата ли клавиша стрелка "вниз". Если это так, то yspeed будет уменьшено. Это заставит частицу двигаться вниз. И снова, задан максимум скорости вниз не больше чем 200.
// Если стрелка вниз и скорость по Y больше чем –200, то увеличим скорость падения
if (keys[VK_DOWN] && (yspeed>-200)) yspeed-=1.0f;
Теперь мы проверим, нажата ли клавиша стрелка вправо. Если это так, то xspeed будет увеличено. Это заставит частицы двигаться вправо. Задан максимум скорости не больше чем 200.
// Если стрелка вправо и X скорость меньше чем 200, то увеличить скорость вправо
if (keys[VK_RIGHT] && (xspeed<200)) xspeed+=1.0f;
Наконец мы проверим, нажата ли клавиша стрелка влево. Если это так... то Вы уже поняли что... xspeed уменьшено, и частицы двигаются влево. Задан максимум скорости не больше чем 200.
// Если стрелка влево и X скорость больше чем –200, то увеличить скорость влево
if (keys[VK_LEFT] && (xspeed>-200)) xspeed-=1.0f;
И последнее, что мы должны сделать - увеличить переменную delay. Я уже говорил, что delay управляет скоростью смены цветов, когда Вы используете режим радуги.
delay++; // Увеличить счетчик задержки циклической смены цветов в режиме радуги
Так же как и во всех предыдущих уроках, проверьте, что заголовок сверху окна правильный.
if (keys[VK_F1]) // Была нажата кнопка F1?
{
keys[VK_F1]=FALSE; // Если так - установим значение FALSE
KillGLWindow(); // Закроем текущее окно OpenGL
fullscreen=!fullscreen; // Переключим режим "Полный экран"/"Оконный"
// Заново создадим наше окно OpenGL
if (!CreateGLWindow("NeHe's Particle Tutorial",640,480,16,fullscreen))
{
return 0; // Выйти, если окно не было создано
}
}
}
}
}
// Сброс
KillGLWindow(); // Закроем окно
return (msg.wParam); // Выйдем из программы
}
В этом уроке, я пробовал детально объяснять все шаги, которые требуются для создания простой, но впечатляющей системы моделирования частиц. Эта система моделирования частиц может использоваться в ваших собственных играх для создания эффектов типа огня, воды, снега, взрывов, падающих звезд, и так далее. Код может быть легко модифицирован для обработки большего количество параметров, и создания новых эффектов (например, фейерверк).
Благодарю Richard Nutman за предложение о том, что частицы можно позиционировать с помощью glVertex3f() вместо сброса матрицы модели просмотра и перепозиционирования каждой частицы с помощью glTranslatef(). Оба метода эффективны, но его метод уменьшил количество вычислений для вывода каждой частицы, что вызвало увеличение быстродействия программы.
Благодарю Antoine Valentim за предложение использовать полоски из треугольников для ускорения программы и введения новой команды в этом уроке. Замечания к этому уроку были великолепными, и я признателен за это!
Я надеюсь, что Вам понравился этот урок. Если Вы что-то не понимаете, или Вы нашли ошибку в этом уроке, пожалуйста, сообщите мне об этом. Я хочу сделать уроки лучше. Ваши замечания очень важны!
Маскирование
Masking
Добро пожаловать на урок номер 20. Растровый формат изображения поддерживается, наверное, на каждом компьютере, и, скорее всего во всех операционных системах. С ним не только легко работать, но и очень просто загружать и использовать как текстуру. До этого урока мы использовали смешивание, чтобы вывести текст на экран и другие изображения без стирания того, что под текстом или изображением. Это эффективно, но результат не всегда удовлетворительный.
В большинстве случаев текстура смешивается излишне или не достаточно хорошо. При разработке игры со спрайтами, Вы не хотите, чтобы сцена за вашим персонажем просвечивала через его тело. Когда вы выводите текст на экран, Вы хотите, чтобы текст был сплошным и легким для чтения.
В данном случае очень пригодиться маскирование. Маскирование – двух шаговый процесс. Вначале мы выводим черно-белое изображение нашей текстуры поверх сцены. Белое - прозрачная часть нашей текстуры. Черное - сплошная часть нашей текстуры. Мы будем использовать такой тип смешивания, при котором только черное будет появляться на сцене. Это похоже на форму для выпечки. Затем мы меняем режим смешивания, и отображаем нашу текстуру поверх, того, что вырезано черным. Опять же, из-за того режима смешивания, который мы используем, только те части нашей текстуры будут скопированы на экран, которые находятся сверху черной маски.
Я приведу весь код в этом уроке кроме тех разделов, которые не изменились. Итак, если Вы готовы научиться кое-чему новому, давайте начнем!
#include <windows.h> // Заголовочный файл для Windows
#include <math.h> // Заголовочный файл для математической библиотеки Windows
#include <stdio.h> // Заголовочный файл для стандартной библиотеки ввода/вывода
#include <gl\gl.h> // Заголовочный файл для библиотеки OpenGL32
#include <gl\glu.h> // Заголовочный файл для библиотеки GLu32
#include <gl\glaux.h> // Заголовочный файл для библиотеки Glaux
HDC hDC=NULL; // Приватный контекст устройства GDI
HGLRC hRC=NULL; // Постоянный контекст визуализации
HWND hWnd=NULL; // Сохраняет дескриптор окна
HINSTANCE hInstance; // Сохраняет экземпляр приложения
Мы будем использовать 7 глобальных переменных в этой программе. masking - логическая переменная (ИСТИНА / ЛОЖЬ), которая будет отслеживать, действительно ли маскировка включена или выключена. mp используется, чтобы быть уверенным, что клавиша 'M' не нажата. sp используется, чтобы быть уверенным, что 'Пробел' не нажат, и переменная scene будет отслеживать, действительно ли мы рисуем первую или вторую сцену.
Мы выделяем память для 5 текстур, используя переменную texture[5]. loop - наш общий счетчик, мы будем использовать его несколько раз в нашей программе, чтобы инициализировать текстуры, и т.д. Наконец, мы имеем переменную roll. Мы будем использовать roll, чтобы сделать прокрутку текстуры по экрану. Создавая изящный эффект! Мы будем также использовать ее для вращения объекта в сцене 2.
bool keys[256]; // Массив для работы с клавиатурой
bool active=TRUE; // Флаг активации окна, по умолчанию = TRUE
bool fullscreen=TRUE;// Флаг полноэкранного режима
bool masking=TRUE; // Маскирование Вкл/Выкл
bool mp; // M нажата?
bool sp; // Пробел нажат?
bool scene; // Какая сцена выводиться
GLuint texture[5]; // Память для пяти наших текстур
GLuint loop; // Общая переменная цикла
GLfloat roll; // Катание текстуры
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление WndProc
Код загрузки картинки не изменился. Он тот же, какой был в уроке 6, и т.д.
В коде ниже мы резервируем память для 5 изображений. Мы очищаем место и загружаем все 5 картинок. Мы делаем цикл по всем изображениям и конвертируем их в текстуры для использования в нашей программе. Текстуры сохранены в texture[0-4].
int LoadGLTextures() // Загрузка картинки и конвертирование в текстуру
{
int Status=FALSE; // Индикатор состояния
AUX_RGBImageRec *TextureImage[5]; // Создать место для текстуры
memset(TextureImage,0,sizeof(void *)*5); // Установить указатель в NULL
if ((TextureImage[0]=LoadBMP("Data/logo.bmp")) && // Текстура эмблемы
(TextureImage[1]=LoadBMP("Data/mask1.bmp")) && // Первая маска
(TextureImage[2]=LoadBMP("Data/image1.bmp")) && // Первое изображение
(TextureImage[3]=LoadBMP("Data/mask2.bmp")) && // Вторая маска
(TextureImage[4]=LoadBMP("Data/image2.bmp"))) // Второе изображение
{
Status=TRUE; // Задать статус в TRUE
glGenTextures(5, &texture[0]); // Создать пять текстур
for (loop=0; loop<5; loop++) // Цикл по всем пяти текстурам
{
glBindTexture(GL_TEXTURE_2D, texture[loop]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop]->sizeX, TextureImage[loop]->sizeY,
0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop]->data);
}
}
for (loop=0; loop<5; loop++) // Цикл по всем пяти текстурам
{
if (TextureImage[loop]) // Если текстура существуют
{
if (TextureImage[loop]->data) // Если изображение текстуры существует
{
free(TextureImage[loop]->data); // Освободить память изображения
}
free(TextureImage[loop]); // Освободить структуру изображения
}
}
return Status; // Возвращаем статус
}
Код ReSizeGLScene() не изменился, и мы опустим его.
Код инициализации необходимая формальность. Мы загружаем наши текстуры, задаем цвет очистки, задаем и разрешаем тест глубины, включаем плавное закрашивание, и разрешаем наложение текстуры. У нас простая программа, поэтому нет необходимости в сложной инициализации :).
int InitGL(GLvoid) // Все начальные настройки OpenGL здесь
{
if (!LoadGLTextures()) // Переход на процедуру загрузки текстуры
{
return FALSE; // Если текстура не загружена возвращаем FALSE
}
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Черный фон
glClearDepth(1.0); // Установка буфера глубины
glEnable(GL_DEPTH_TEST); // Разрешение теста глубины
glShadeModel(GL_SMOOTH); // Разрешить плавное закрашивание
glEnable(GL_TEXTURE_2D); // Разрешение наложения текстуры
return TRUE; // Инициализация завершена OK
}
Теперь самое интересное. Наш код рисования! Мы начинаем как обычно. Мы очищаем фон и буфер глубины. Затем мы сбрасываем матрицу вида, и перемещаемся на 2 единицы вглубь экрана так, чтобы мы могли видеть нашу сцену.
int DrawGLScene(GLvoid) // Здесь мы все рисуем
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экрана и буфера глубины
glLoadIdentity(); // Сброс матрицы вида
glTranslatef(0.0f,0.0f,-2.0f); // Перемещение вглубь экрана на 2 единицы
Первая строка ниже выбирает текстуру 'эмблемы' сайта NeHe. Мы наложим текстуру на экран, используя четырехугольник. Мы задаем четыре текстурных координаты совместно с четырьмя вершинами.
Дополнение от Джонатана Роя: помните, что OpenGL - графическая система на основе вершин. Большинство параметров, которые Вы задаете, регистрируются как атрибуты отдельных вершин. Текстурные координаты - один из таких атрибутов. Вы просто задаете соответствующие текстурные координаты для каждой вершины многоугольника, и OpenGL автоматически заполняет поверхность между вершинами текстурой, в процессе известном как интерполяция. Интерполяция - стандартная геометрическая техника, которая позволяет OpenGL определить, как данный параметр изменяется между вершинами, зная только значение, которое параметр имеет в вершинах непосредственно.
Как и в предыдущих уроках, мы представим, что мы на внешней стороне четырехугольника и назначаем координаты текстуры следующим образом: (0.0, 0.0) нижний левый угол, (0.0, 1.0) верхний левый угол, (1.0, 0.0) нижний правый, и (1.0, 1.0) верхний правый. А теперь, с учетом этой настройки, Вы можете указать, какие координаты текстуры соответствуют точке в середине четырехугольника? Правильно, (0.5, 0.5). Но в коде Вы ни где не задавали эту координату, не так ли? Когда рисуется четырехугольник, OpenGL вычисляет это за Вас. И просто волшебство то, что он это делает безотносительно к позиции, размера, или ориентации многоугольника!
В этом уроке мы привнесем еще один интересный трюк, назначив текстурные координаты, которые будут отличаться от 0.0 и 1.0. Текстурные координаты должны быть нормализованы. Значение 0.0 отображается на одну грань текстуры, в тоже время как значение 1.0 отображает на противоположную грань, захватывая всю ширину или высоту изображения текстуры за одну единицу, независимо от размера многоугольника или размера изображения текстуры в пикселях (о чем мы не должны волноваться при выполнении наложения текстуры, и это делает жизнь в целом несколько проще). Значения большие, чем 1.0, будут просто заворачивать наложение с другой грани и повторять текстуру. Другими словами, например, текстурная координата (0.3, 0.5) отображает точно тот же самый пиксель в изображении текстуры, как и координата (1.3, 0.5), или как (12.3,-2.5). В этом уроке, мы добьемся мозаичного эффекта, задавая значение 3.0 вместо 1.0, повторяя текстуру девять раз (3x3 мозаика) по поверхности четырехугольника.
Дополнительно, мы используем переменную roll, чтобы заставить текстуру перемещаться (или скользить) по поверхности четырехугольника. Значение 0.0 для roll, которое добавлено к вертикальной координате текстуры, означает, что наложение текстуры на нижнею грань четырехугольника начинается на нижней грани изображения текстуры, как показано на рисунке слева. Когда roll равна 0.5, наложение на нижнею грань четырехугольника начинается с половины изображении (см. рисунок справа). Прокрутка текстуры может использоваться, чтобы создать отличные эффекты типа движущихся облаков, вращающихся слов вокруг объектов, и т.д.
glBindTexture(GL_TEXTURE_2D, texture[0]); // Выбор текстуры эмблемы
glBegin(GL_QUADS); // Начало рисования текстурного четырехугольника
glTexCoord2f(0.0f, -roll+0.0f); glVertex3f(-1.1f, -1.1f, 0.0f); // Лево Низ
glTexCoord2f(3.0f, -roll+0.0f); glVertex3f( 1.1f, -1.1f, 0.0f); // Право Низ
glTexCoord2f(3.0f, -roll+3.0f); glVertex3f( 1.1f, 1.1f, 0.0f); // Право Верх
glTexCoord2f(0.0f, -roll+3.0f); glVertex3f(-1.1f, 1.1f, 0.0f); // Лево Верх
glEnd(); // Завершения рисования четырехугольника
Продолжим… Теперь мы разрешаем смешивание. Чтобы этот эффект работал мы также должны отключить тест глубины. Очень важно, чтобы Вы это сделали! Если Вы не отключили тест глубины, вероятно, вы ничего не увидите. Все Ваше изображение исчезнет!
glEnable(GL_BLEND); // Разрешение смешивания
glDisable(GL_DEPTH_TEST); // Запрет теста глубины
Первое, что мы делаем после того, как мы разрешаем смешивание и отключаем тест глубины – проверка надо ли нам маскировать наше изображение или смешивать его на старый манер. Строка кода ниже проверяет истина ли маскировка. Если это так, то мы задаем смешивание таким образом, чтобы наша маска выводилась на экран правильным образом.
if (masking) // Маскировка разрешена?
{
Если маскировка ИСТИНА, что строка ниже задаст смешивание для нашей маски. Маска – это только черно-белая копия текстуры, которую мы хотим вывести на экран. Любая белая часть маски будет прозрачной. Любая черная часть маски будет непрозрачной.
Команда настройки смешивания ниже делает следующее: цвет адресата (цвет на экране) будет установлен в черный, если часть нашей маски, которая копируется на экран, черная. Это означает, что часть экрана, которая попадает под черную часть нашей маски, станет черной. Все, что было на экране под маской, будет очищено в черный цвет. Часть экрана, попавшего под белую маску не будет изменена.
glBlendFunc(GL_DST_COLOR,GL_ZERO); // Смешивание цвета экрана с нулем (Черное)
}
Теперь мы проверим, какую сцену надо вывести. Если scene ИСТИНА, то мы выведем вторую сцену. Если scene ЛОЖЬ, то мы выведем первую сцену.
if (scene) // Рисовать вторую сцену?
{
Мы не хотим, чтобы объекты были слишком большими, поэтому мы перемещаемся еще на одну единицу в экран. Это уменьшит размер наших объектов.
После того, как мы переместились в экран, мы вращаемся от 0-360 градусов в зависимости от значения roll. Если roll - 0.0, мы будем вращать на 0 градусов. Если roll - 1.0, мы будем вращать на 360 градусов. Довольно быстрое вращение, но я не хочу создавать другую переменную только, для того чтобы вращать изображение в центре экрана. :)
glTranslatef(0.0f,0.0f,-1.0f); // Перемещение вглубь экрана на одну единицу
glRotatef(roll*360,0.0f,0.0f,1.0f); // Вращение по оси Z на 360 градусов
Мы уже имеем прокрутку эмблемы на экране, и мы вращаем сцену по оси Z, при этом любые объекты, которые мы рисуем, вращаются против часовой стрелки, теперь все, что мы должны сделать – это проверить, включена ли маскировка. Если это так, то мы выведем нашу маску, затем наш объект. Если маскировка выключена, то мы выведем только наш объект.
if (masking) // Маскирование включено?
{
Если маскировка ИСТИНА, то код ниже выведет нашу маску на экран. Наш режим смешивания уже задан ранее. Теперь все, что мы должны сделать – это вывести маску на экран. Мы выбираем маску 2 (потому что это вторая сцена). После того, как мы выбрали текстуру маски, мы накладываем текстуру на четырехугольник. Четырехугольник размером на 1.1 единицу влево и вправо так, чтобы он немного выходил за край экрана. Мы хотим показать только одну текстуру, поэтому координаты текстуры изменяются от 0.0 до 1.0.
После отрисовки нашей маски на экране будет находиться сплошная черная копия нашей завершающей текстуры. Это похоже на то, что формочка для выпечки, которая по форме совпадает с нашей завершающей текстурой, вырезала на экране пустое черное место.
glBindTexture(GL_TEXTURE_2D, texture[3]); // Выбор второй маски текстуры
glBegin(GL_QUADS); // Начало рисования текстурного четырехугольника
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.1f, -1.1f, 0.0f); // Низ Лево
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.1f, -1.1f, 0.0f); // Низ Право
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.1f, 1.1f, 0.0f); // Верх Право
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.1f, 1.1f, 0.0f); // верх Лево
glEnd(); // Конец рисования четырехугольника
}
Теперь, когда мы вывели нашу маску на экран, пришло время снова изменить режим смешивания. На сей раз, мы собираемся, указать OpenGL, что надо копировать на экран любую часть нашей цветной текстуры, которая НЕ черная. Поскольку завершающая текстура - точная копия маски, но с цветом, выводятся на экран только те части нашей текстуры, которые попадают сверху черной части маски. Поскольку маска черная, ничто с экрана не будет просвечивать через нашу текстуру. И с нами остается сплошная текстура, плавающая сверху по экрану.
Заметьте, что мы выбираем второе изображение после выбора завершающего режима смешивания. При этом выбирается наше цветное изображение (изображение, на котором основана вторая маска). Также заметьте, что мы выводим это изображения с правого верхнего угла маски. Те же самые текстурные координаты, те же самые вершины.
Если мы не выведем маску, наше изображение будет скопировано на экран, но оно смешает с тем, что было на экране.
glBlendFunc(GL_ONE, GL_ONE); // Копирование цветного изображения 2 на экран
glBindTexture(GL_TEXTURE_2D, texture[4]); // Выбор второго изображения текстуры
glBegin(GL_QUADS); // Начало рисования текстурного четырехугольника
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.1f, -1.1f, 0.0f); // Низ Лево
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.1f, -1.1f, 0.0f); // Низ Право
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.1f, 1.1f, 0.0f); // Верх Право
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.1f, 1.1f, 0.0f); // Верх Лево
glEnd(); // Завершение рисования четырехугольника
}
Если scene была ЛОЖЬ, мы выведем первую сцену (моя любимая).
else // Иначе
{
Вначале мы проверяем, включена ли маскировка, точно так же как в коде выше.
if (masking) // Вкл. маскировка?
{
Если masking ИСТИНА, то мы выводим нашу маску 1 на экран (маска для сцены 1). Заметьте, что текстура прокручивается справа налево (roll добавляется к горизонтальной координате текстуры). Мы хотим, чтобы эта текстура заполнила весь экран, именно поэтому мы и не перемещаемся глубже в экран.
glBindTexture(GL_TEXTURE_2D, texture[1]); // Выбор первой маски текстуры
glBegin(GL_QUADS); // Начало рисования текстурного четырехугольника
glTexCoord2f(roll+0.0f, 0.0f); glVertex3f(-1.1f, -1.1f, 0.0f); // Низ Лево
glTexCoord2f(roll+4.0f, 0.0f); glVertex3f( 1.1f, -1.1f, 0.0f); // Низ Право
glTexCoord2f(roll+4.0f, 4.0f); glVertex3f( 1.1f, 1.1f, 0.0f); // Верх Право
glTexCoord2f(roll+0.0f, 4.0f); glVertex3f(-1.1f, 1.1f, 0.0f); // Верх Лево
glEnd(); // Конец рисования четырехугольника
}
Снова мы разрешаем смешивание и выбираем нашу текстуру для сцены 1. Мы накладываем эту текстуру поверх маски. Заметьте, что мы прокручиваем эту текстуру таким же образом, иначе маска и завершающие изображение не совместились.
glBlendFunc(GL_ONE, GL_ONE); // Копирование цветного изображения 1 на экран
glBindTexture(GL_TEXTURE_2D, texture[2]); // Выбор первого изображения текстуры
glBegin(GL_QUADS); // Начало рисования текстурного четырехугольника
glTexCoord2f(roll+0.0f, 0.0f); glVertex3f(-1.1f, -1.1f, 0.0f); // Низ Лево
glTexCoord2f(roll+4.0f, 0.0f); glVertex3f( 1.1f, -1.1f, 0.0f); // Низ Право
glTexCoord2f(roll+4.0f, 4.0f); glVertex3f( 1.1f, 1.1f, 0.0f); // Верх Право
glTexCoord2f(roll+0.0f, 4.0f); glVertex3f(-1.1f, 1.1f, 0.0f); // Верх Лево
glEnd(); // Конец рисования четырехугольника
}
Затем мы разрешаем тест глубины, и отключаем смешивание. Это предотвращает странные вещи, происходящие от случая к случаю в остальной части нашей программы. :)
glEnable(GL_DEPTH_TEST); // Разрешение теста глубины
glDisable(GL_BLEND); // Запрещение смешивания
В завершении надо увеличить значение roll. Если roll больше, чем 1.0, мы вычитаем 1.0. Это предотвращает появление больших значений roll.
roll+=0.002f; // Увеличим прокрутку нашей текстуры
if (roll>1.0f) // Roll больше чем
{
roll-=1.0f; // Вычтем 1 из Roll
}
return TRUE; // Все OK
}
Код KillGLWindow(), CreateGLWindow() и WndProc() не изменился, поэтому мы опустим его.
Первое что изменилось в WinMain() - заголовок окна. Теперь название "Урок Маскирования NeHe". Вы можете изменить это название на такое, какое Вы захотите. :)
int WINAPI WinMain(
HINSTANCE hInstance, // Экземпляр
HINSTANCE hPrevInstance, // Предыдущий экземпляр
LPSTR lpCmdLine, // Параметры командной строки
int nCmdShow) // Показать состояние окна
{
MSG msg; // Структура сообщения окна
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 Masking Tutorial",640,480,16,fullscreen))
{
return 0; // Выходим если окно не было создано
}
while (!done) // Цикл, который продолжается пока done=FALSE
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Есть ожидаемое сообщение?
{
if (msg.message==WM_QUIT) // Мы получили сообщение о выходе?
{
done=TRUE; // Если так done=TRUE
}
else // Если нет, продолжаем работать с сообщениями окна
{
TranslateMessage(&msg); // Переводим сообщение
DispatchMessage(&msg); // Отсылаем сообщение
}
}
else // Если сообщений нет
{
// Рисуем сцену. Ожидаем нажатия кнопки ESC и сообщения о выходе от DrawGLScene()
// Активно? Было получено сообщение о выходе?
if ((active && !DrawGLScene()) || keys[VK_ESCAPE])
{
done=TRUE; // ESC или DrawGLScene просигналили "выход"
}
else // Не время выходить, обновляем экран
{
SwapBuffers(hDC); // Переключаем буферы (Двойная буферизация)
Теперь наш простой обработчик клавиатуры. Мы проверяем, нажат ли пробел. Если это так, то мы устанавливаем переменную sp в ИСТИНА. Если sp ИСТИНА, код ниже не будет выполняться второй раз, пока пробел не был отпущен. Это блокирует быстрое переключение сцен в нашей программе. После того, как мы устанавливаем sp в ИСТИНА, мы переключаем сцену. Если scene была ИСТИНА, она станет ЛОЖЬ, если она была ЛОЖЬ, то станет ИСТИНА. В нашем коде рисования выше, если scene ЛОЖЬ, первая сцена будет выведена. Если scene ИСТИНА, то вторая сцена будет выведена.
if (keys[' '] && !sp) // Пробел нажат?
{
sp=TRUE; // Сообщим программе, что пробел нажат
scene=!scene; // Переключение сцен
}
Код ниже проверяет, отпустили ли мы пробел (если НЕ ' '). Если пробел был отпущен, мы устанавливаем sp в ЛОЖЬ, сообщая нашей программе, что пробел не нажат. Задав sp в ЛОЖЬ, код выше снова проверит, был ли нажат пробел, и если так, то все повториться.
if (!keys[' ']) // Пробел отжат?
{
sp=FALSE; // Сообщим программе, что пробел отжат
}
В следующем разделе кода проверяется нажатие клавиши 'M'. Если она нажата, мы устанавливаем mp в ИСТИНА, указывая программе не проверять это условие в дальнейшем, пока клавиша не отпущена, и мы переключаем masking с ИСТИНА на ЛОЖЬ или с ЛОЖЬ на ИСТИНА. Если masking ИСТИНА, то в коде рисования будет подключена маскировка. Если masking ЛОЖЬ, то маскировка будет отключена. Если маскировка выключена, то объект будет смешан с содержимым экрана, используя смешивание на старый манер, который мы использовали до сих пор.
if (keys['M'] && !mp) // M нажата?
{
mp=TRUE; // Сообщим программе, что M нажата
masking=!masking; // Переключение режима маскирования Выкл/Вкл
}
Последняя небольшая часть кода проверяет, отпущена ли "M". Если это так, то mp присваивается ЛОЖЬ, давая знать программе, что мы больше не нажимаем клавишу 'M'. Как только клавиша 'M' была отпущена, мы можем нажать ее еще раз, чтобы переключить включение или отключение маскировки.
if (!keys['M']) // M отжата?
{
mp=FALSE; // Сообщим программе, что M отжата
}
Как и в других уроках, удостоверитесь, что заголовок наверху окна правильный.
if (keys[VK_F1]) // Была нажата кнопка F1?
{
keys[VK_F1]=FALSE; // Если так - установим значение FALSE
KillGLWindow(); // Закроем текущее окно OpenGL
fullscreen=!fullscreen; // Переключим режим "Полный экран"/"Оконный"
// Заново создадим наше окно OpenGL
if (!CreateGLWindow("NeHe's Masking Tutorial",640,480,16,fullscreen))
{
return 0; // Выйти, если окно не было создано
}
}
}
}
}
// Сброс
KillGLWindow(); // Закроем окно
return (msg.wParam); // Выйдем из программы
}
Создание маски не сложно, и не требует много времени. Лучший способ сделать маску из готового изображения, состоит в том, чтобы загрузить ваше изображение в графический редактор или программу просмотра изображений, такую как infranview, и перевести изображение в серую шкалу. После того, как Вы сделали это, увеличивайте контрастность так, чтобы серые пиксели стали черными. Вы можете также уменьшить яркость, и т.д. Важно, что белый цвет это ярко белый, и черный это чисто черный. Если Вы имеете любые серые пиксели в вашей маске, то эта часть изображения будет прозрачной. Наиболее надежный способ удостовериться, что ваша маска точная копия вашего изображения, снять копию изображения с черным. Также очень важно, что ваше изображение имеет ЧЕРНЫЙ цвет, и маска имеет БЕЛЫЙ цвет! Если Вы создали маску и заметили квадратную форму вокруг вашей текстуры, или ваш белый - не достаточно яркий (255 или FFFFFF) или ваш черный - не точно черный (0 или 000000). Ниже Вы приведен пример маски и изображения, которое накладывается поверх маски. Изображение может иметь любой цвет, который Вы хотите, но фон должен быть черный. Маска должна иметь белый фон и черную копию вашего изображения.
Это - маска - > . Это - изображение - > .
(Прим.переводчика: Вы можете в изображении назначить любой цвет фоновым, важно, чтобы он стал белым в маске, а все остальные цвета перешли в черный цвет. Можно воспользоваться для выделения прозрачного цвета (или группы цветов) инструментов Select/Color Range в AdobePhotoshop, а затем залить выделенную область в белый цвет (тем самым вы создадите прозрачные области в маски), а затем инвертировать эту область и залить ее черным цветом (тем самым вы создадите непрозрачные области в маске).
Эрик Десросиерс подсказал, что Вы можете также проверять значение каждого пикселя в вашем рисунке, во время его загрузки. Если Вы хотите иметь прозрачный пиксель, Вы можете присвоить ему альфа значение 0. Для всех других цветов Вы можете присвоить им альфа значение 255. Этот метод будет также работать, но требует дополнительного кодирования. Текущий урок прост и требует очень немного дополнительного кода. Я не отвергаю другие методы, но когда я писал обучающую программу, я пробовал сделать код простым, понятым и удобным. Я только хотел сказать, что есть всегда другие способы сделать эту работу. Спасибо за замечание Эрик.
В этом уроке я показал Вам простой, но эффективный способ рисования частей текстуры на экран без использования альфа канала. Стандартное смешивание обычно выглядит плохо (текстуры или прозрачные, или они не прозрачные), и текстурирование с альфа каналом требует, чтобы ваши изображения имели альфа канал. С растровыми изображениями удобно работать, но они не поддерживают альфа канала, эта программа показывает нам, как обойти ограничения растровых изображений, демонстрируя крутой способ создавать эффекты типа штампа (effect overlay).
Благодарю Роба Санте за идею и за пример кода. Я никогда не слышал об этом небольшом трюке, пока он не указал на него. Он хотел, чтобы я подчеркнул, что, хотя эта уловка и работает, но для нее требуется два прохода, и это снижается производительность. Он рекомендует, чтобы Вы использовали текстуры с альфа каналом для сложных сцен.
Я надеюсь, что Вам понравится этот урок. Если Вы что-то не понимаете, или Вы нашли ошибку в уроке, пожалуйста, сообщите мне. Я хочу сделать уроки лучше. Ваши замечания также очень важны!
Наложение микрорельефа
Bump-Mapping, Multi-Texturing & Extensions
Этот урок, написанный Дженсом Шнайдером (Jens Schneider), основан на материале Урока 6, но содержит существенные изменения и дополнения. Здесь вы узнаете:
как управлять функциями мультитекстурирования видеокарты;
как выполнять "поддельное" наложение микрорельефа методом тиснения;
как, используя смешивание, отображать эффектно смотрящиеся логотипы, "летающие" по просчитанной сцене;
как просто и быстро выполнять преобразование матриц;
познакомитесь с основами техники многопроходной визуализации.
По меньшей мере, три из перечисленных пунктов могут быть отнесены к "продвинутым техникам текстурирования", поэтому для работы с ними нужно хорошо понимать основы функционирования конвейера визуализации в OpenGL. Требуется знать большинство команд, изученных в предыдущих уроках и быть достаточно близко знакомым с векторной математикой. Иногда будут попадаться блоки, озаглавленные "начало теории(…)" и оканчивающиеся фразой "конец теории(…)". В таких местах рассматриваются теоретические основы вопросов, указанных в скобках, и если вы их знаете, то можете пропустить. Если возникают проблемы с пониманием кода, лучше вернуться к теоретической части и попробовать разобраться. И еще: в уроке более 1200 строк кода, значительные фрагменты которого очевидны и скучны для тех, кто читал предыдущие главы. Поэтому я не стал комментировать каждую строку, пояснил только главное. Если встретите что-то вроде >…<, это значит, что строки кода были пропущены.
Итак:
#include <windows.h> // Файл заголовков функций Windows
#include <stdio.h> // Файл заголовков для библиотеки ввода-вывода
#include <gl\gl.h> // Файл заголовков для библиотеки OpenGL32
#include <gl\glu.h> // Файл заголовков для библиотеки GLu32
#include <gl\glaux.h> // Файл заголовков для библиотеки GLaux
#include "glext.h" // Файл заголовков для мультитекстурирования
#include <string.h> // Файл заголовков для работы со строками
#include <math.h> // Файл заголовков для математической библиотеки
Параметр GLfloat MAX_EMBOSS задает "интенсивность" эффекта рельефности. Увеличение этого числа значительно усиливает эффект, но приводит к снижению качества и появлению так называемых "артефактов" изображения по краям поверхностей.
// Коэффициент рельефности. Увеличьте, чтобы усилить эффект
#define MAX_EMBOSS (GLfloat)0.008f
Давайте подготовимся к использованию расширения GL_ARB_multitexture. Это просто.
В настоящее время подавляющая часть акселераторов имеет более одного блока текстурирования на чипе. Чтобы определить, верно ли это для используемой карточки, надо проверить ее на поддержку опции GL_ARB_multitexture, которая позволяет накладывать две или более текстур на примитив за один проход. Звучит не слишком впечатляюще, но на самом деле это мощный инструмент! Практически любая сцена выглядит гораздо красивее, если ее на объекты наложено несколько текстур. Обычно для этого требуется сделать несколько "проходов", состоящих из выбора текстуры и отрисовки геометрии; при увеличении числа таких операций работа серьезно тормозится. Однако не беспокойтесь, позже все прояснится.
Вернемся к коду: __ARB_ENABLE используется, чтобы при необходимости отключить мультитекстурирование. Если хотите видеть OpenGL-расширения, раскомментируйте строку #define EXT_INFO. Доступность расширений будет проверяться во время выполнения, чтобы сохранить переносимость кода, поэтому нужны будут несколько переменных строкового типа — они заданы двумя следующими строками. Кроме того, желательно различать доступность мультитекстурирования и его использование, то есть нужны еще два флага. Наконец, нужно знать, сколько блоков текстурирования доступно (хотя мы будем использовать всего два). По меньшей мере один такой блок обязательно присутствует на любой OpenGL-совместимой карте, так что переменную maxTexelUnits надо инициализировать единичкой.
#define __ARB_ENABLE true // Используется, чтобы полностью отключить расширения
// #define EXT_INFO // Раскомментируйте, чтобы увидеть при запуске доступные расширения
#define MAX_EXTENSION_SPACE 10240 // Символы строк-описателей расширений
#define MAX_EXTENSION_LENGTH 256 // Максимальное число символов в одной строке-описателе
bool multitextureSupported=false; // Флаг, определяющий, поддерживается ли мультитекстурирование
bool useMultitexture=true; // Использовать его, если оно доступно?
GLint maxTexelUnits=1; // Число текстурных блоков. Как минимум 1 есть всегда
Следующие строки нужны, чтобы сопоставить расширениям соответствующие вызовы функций C++. Просто считайте, что PFN-и-как-там-дальше — предварительно определенный тип данных, нужный для описания вызовов функций. Мы не уверены, что к этим прототипам будут доступны функции, а потому установим их в NULL. Команды glMultiTexCoordifARB задают привязку к хорошо известным glTexCoordif, описывающим i-мерные текстурные координаты. Заметьте, что они могут полностью заменить команды, связанные с glTexCoordif. Мы пользуемся версиями с GLfloat, и нам нужны прототипы тех команд, которые оканчиваются на "f"; другие команды при этом также остаются доступны ("fv", "i" и т.д.). Два последних прототипа задают функцию выбора активного блока текстурирования (texture-unit), занятого привязкой текстур ( glActiveTextureARB() ), и функцию, определяющую, какой из текстурных блоков связан с командой выбора указателя на массив (glClientActiveTextureARB). К слову: ARB — это сокращение от "Architectural Review Board", "комитет по архитектуре". Расширения, содержащие в имени строку ARB, не требуются для реализации системы, соответствующей спецификации OpenGL, но ожидается, что такие расширения найдут широкую поддержку у производителей. Пока ARB-статус имеют только расширения, связанные с мультитекстурированием. Такая ситуация, скорее всего, указывает на то, что мультитекстурирование наносит страшный удар по производительности, когда дело касается некоторых продвинутых техник визуализации.
Пропущенные строки относятся к указателям на контекст GDI и прочему.
PFNGLMULTITEXCOORD1FARBPROC glMultiTexCoord1fARB = NULL;
PFNGLMULTITEXCOORD2FARBPROC glMultiTexCoord2fARB = NULL;
PFNGLMULTITEXCOORD3FARBPROC glMultiTexCoord3fARB = NULL;
PFNGLMULTITEXCOORD4FARBPROC glMultiTexCoord4fARB = NULL;
PFNGLACTIVETEXTUREARBPROC glActiveTextureARB = NULL;
PFNGLCLIENTACTIVETEXTUREARBPROC glClientActiveTextureARB= NULL;
Создаем глобальные переменные:
filter задает используемый фильтр (см. Урок 06). Обычно берем GL_LINEAR, поэтому инициализируем переменную единичкой.
texture хранит текстуру, три копии, по одной на фильтр.
bump хранит карты микрорельефа.
invbump хранит инвертированную карту микрорельефа. Причина объясняется позже, в теоретическом разделе.
Переменные, относящиеся к логотипам, в имени которых есть слово "Logo" - хранят текстуры, добавляемые к сцене на последнем проходе.
Переменные, относящиеся к свету, в имени которых есть слово “Light” - хранят параметры источника света.
GLuint filter=1; // Какой фильтр использовать
GLuint texture[3]; // Хранит 3 текстуры
GLuint bump[3]; // Рельефы
GLuint invbump[3]; // Инвертированные рельефы
GLuint glLogo; // Указатель на OpenGL-логотип
GLuint multiLogo; // Указатель на мультитекстурированный логотип
GLfloat LightAmbient[] = { 0.2f, 0.2f, 0.2f}; // Фоновое освещение — 20% белого
GLfloat LightDiffuse[] = { 1.0f, 1.0f, 1.0f}; // Рассеянный свет — чисто белый
GLfloat LightPosition[] = { 0.0f, 0.0f, 2.0f}; // Положение источника — перед экраном
GLfloat Gray[] = { 0.5f, 0.5f, 0.5f, 1.0f};
Очередной фрагмент кода содержит числовое описание текстурированного куба, сделанного из GL_QUADS-ов. Каждые пять чисел представляют собой пару из двумерных текстурных и трехмерных вершинных координат. Это удобно для построения куба в цикле for…, учитывая, что нам потребуется сделать это несколько раз. Блок данных заканчивается прототипом функции WndProc(), хорошо известной из предыдущих уроков.
// Данные содержат грани куба в формате "2 текстурные координаты, 3 вершинные".
// Обратите внимание, что мозаичность куба минимальна.
GLfloat data[]= {
// ЛИЦЕВАЯ ГРАНЬ
0.0f, 0.0f, -1.0f, -1.0f, +1.0f,
1.0f, 0.0f, +1.0f, -1.0f, +1.0f,
1.0f, 1.0f, +1.0f, +1.0f, +1.0f,
0.0f, 1.0f, -1.0f, +1.0f, +1.0f,
// ЗАДНЯЯ ГРАНЬ
1.0f, 0.0f, -1.0f, -1.0f, -1.0f,
1.0f, 1.0f, -1.0f, +1.0f, -1.0f,
0.0f, 1.0f, +1.0f, +1.0f, -1.0f,
0.0f, 0.0f, +1.0f, -1.0f, -1.0f,
// ВЕРХНЯЯ ГРАНЬ
0.0f, 1.0f, -1.0f, +1.0f, -1.0f,
0.0f, 0.0f, -1.0f, +1.0f, +1.0f,
1.0f, 0.0f, +1.0f, +1.0f, +1.0f,
1.0f, 1.0f, +1.0f, +1.0f, -1.0f,
// НИЖНЯЯ ГРАНЬ
1.0f, 1.0f, -1.0f, -1.0f, -1.0f,
0.0f, 1.0f, +1.0f, -1.0f, -1.0f,
0.0f, 0.0f, +1.0f, -1.0f, +1.0f,
1.0f, 0.0f, -1.0f, -1.0f, +1.0f,
// ПРАВАЯ ГРАНЬ
1.0f, 0.0f, +1.0f, -1.0f, -1.0f,
1.0f, 1.0f, +1.0f, +1.0f, -1.0f,
0.0f, 1.0f, +1.0f, +1.0f, +1.0f,
0.0f, 0.0f, +1.0f, -1.0f, +1.0f,
// ЛЕВАЯ ГРАНЬ
0.0f, 0.0f, -1.0f, -1.0f, -1.0f,
1.0f, 0.0f, -1.0f, -1.0f, +1.0f,
1.0f, 1.0f, -1.0f, +1.0f, +1.0f,
0.0f, 1.0f, -1.0f, +1.0f, -1.0f
};
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление WndProc
В следующем блоке кода реализована проверка поддержки расширений во время выполнения.
Во-первых, предположим, что у нас есть длинная строка, содержащая список всех поддерживаемых расширений, представленных в виде подстрок, разделенных символом ‘\n’. Таким образом, надо провести поиск этого символа и начать сравнение string с search до достижения либо очередного ‘\n’, либо отличия в сравниваемых строках. В первом случае вернем в "найдено" значение true, во втором — возьмем следующую подстроку, и так до тех пор, пока не кончится string. Со string придется немного повозиться, поскольку она начинается не с символа ‘\n’.
Кстати: проверку доступности любого данного расширения во время выполнения программы надо выполнять ВСЕГДА!
bool isInString(char *string, const char *search) {
int pos=0;
int maxpos=strlen(search)-1;
int len=strlen(string);
char *other;
for (int i=0; i<len; i++) {
if ((i==0) || ((i>1) && string[i-1]=='\n')) { // Новые расширения начинаются здесь!
other=&string[i];
pos=0; // Начать новый поиск
while (string[i]!='\n') { // Поиск по всей строке расширения
if (string[i]==search[pos]) pos++; // Следующий символ
if ((pos>maxpos) && string[i+1]=='\n') return true; // А вот и она!
i++;
}
}
}
return false; // Простите, не нашли!
}
Теперь извлечем строку расширений и преобразуем ее в строки, разделенные символом ‘\n’, чтобы провести поиск. Если будет обнаружена строка ”GL_ARB_multitexture”, значит, эта опция поддерживается. Но чтобы ее использовать, нужно, во-первых, чтобы __ARB_ENABLE была установлена в true, а во-вторых, чтобы карточка поддерживала расширение GL_EXT_texture_env_combine, которое указывает, что аппаратура разрешает некоторые новые способы взаимодействия между своими текстурными блоками. Это необходимо, поскольку GL_ARB_multitexture обеспечивает лишь вывод обработанных данных последовательно с текстурного блока с меньшим номером на блок с большим, а поддержка GL_EXT_texture_env_combine означает возможность использования уравнений смешивания повышенной сложности, эффект от которых совсем другой. Если все необходимые расширения поддерживаются и мы не запретили их сами, определим количество доступных текстурных блоков. Это число будет храниться в maxTexelUnits. Затем установим связь между функциями и их именами, для этого воспользуемся вызовом wglGetProcAdress(), передавая ей в качестве параметра строку-имя искомой функции и проводя преобразование типа результата, чтобы гарантировать совпадение ожидаемого и полученного типов.
bool initMultitexture(void) {
char *extensions;
extensions=strdup((char *) glGetString(GL_EXTENSIONS)); // Получим строку расширений
int len=strlen(extensions);
for (int i=0; i<len; i++) // Разделим ее символами новой строки вместо пробелов
if (extensions[i]==' ') extensions[i]='\n';
#ifdef EXT_INFO
MessageBox(hWnd,extensions,"поддерживаются расширения GL:",MB_OK | MB_ICONINFORMATION);
#endif
if (isInString(extensions,"GL_ARB_multitexture") // Мультитекстурирование поддерживается?
&& __ARB_ENABLE // Проверим флаг
// Поддерживается среда комбинирования текстур?
&& isInString(extensions,"GL_EXT_texture_env_combine"))
{
glGetIntegerv(GL_MAX_TEXTURE_UNITS_ARB,&maxTexelUnits);
glMultiTexCoord1fARB = (PFNGLMULTITEXCOORD1FARBPROC) wglGetProcAddress("glMultiTexCoord1fARB");
glMultiTexCoord2fARB = (PFNGLMULTITEXCOORD2FARBPROC) wglGetProcAddress("glMultiTexCoord2fARB");
glMultiTexCoord3fARB = (PFNGLMULTITEXCOORD3FARBPROC) wglGetProcAddress("glMultiTexCoord3fARB");
glMultiTexCoord4fARB = (PFNGLMULTITEXCOORD4FARBPROC) wglGetProcAddress("glMultiTexCoord4fARB");
glActiveTextureARB = (PFNGLACTIVETEXTUREARBPROC) wglGetProcAddress("glActiveTextureARB");
glClientActiveTextureARB= (PFNGLCLIENTACTIVETEXTUREARBPROC) wglGetProcAddress("glClientActiveTextureARB");
#ifdef EXT_INFO
MessageBox(hWnd,"Будет использовано расширение GL_ARB_multitexture.",
"опция поддерживается!",MB_OK | MB_ICONINFORMATION);
#endif
return true;
}
useMultitexture=false;// Невозможно использовать то, что не поддерживается аппаратурой
return false;
}
InitLights() инициализирует освещение OpenGL, будучи вызвана позже из InitGL().
void initLights(void) {
// Загрузка параметров освещения в GL_LIGHT1
glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);
glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse);
glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);
glEnable(GL_LIGHT1);
}
Здесь грузится УЙМА текстур. Поскольку у функции auxDIBImageLoad() есть собственный обработчик ошибок, а LoadBMP() труднопредсказуема и требует блока try-catch, я отказался от нее. Но вернемся к процедуре загрузки. Сначала берем базовую картинку и создаем на ее основе три фильтрованных текстуры (в режимах GL_NEAREST, GL_LINEAR и GL_LINEAR_MIPMAP_NEAREST). Обратите внимание, для хранения растра используется лишь один экземпляр структуры данных, поскольку в один момент открытой нужна лишь одна картинка. Здесь применяется новая структура данных, alpha — в ней содержится альфа-слой текстур. Такой подход позволяет хранить RGBA-изображения в виде двух картинок: основного 24-битного RGB растра и 8-битного альфа-канала в шкале серого. Чтобы индикатор состояния работал корректно, нужно удалять Image-блок после каждой загрузки и сбрасывать его в NULL.
Еще одна особенность: при задании типа текстуры используется GL_RGB8 вместо обычного "3". Это сделано для совместимости с будущими версиями OpenGL-ICD и рекомендуется к использованию вместо любого другого числа. Такие параметры я пометил оранжевым.
int LoadGLTextures() { // Загрузка растра и преобразование в текстуры
bool status=true; // Индикатор состояния
AUX_RGBImageRec *Image=NULL; // Создадим место для хранения текстур
char *alpha=NULL;
// Загрузим базовый растр
if (Image=auxDIBImageLoad("Data/Base.bmp")) {
glGenTextures(3, texture); // Создадим три текстуры
// Создаем текстуру с фильтром по ближайшему
glBindTexture(GL_TEXTURE_2D, texture[0]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, Image->sizeX, Image->sizeY, 0,
GL_RGB, GL_UNSIGNED_BYTE, Image->data);
// Создаем текстуру с фильтром усреднения
glBindTexture(GL_TEXTURE_2D, texture[1]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, Image->sizeX, Image->sizeY, 0,
GL_RGB, GL_UNSIGNED_BYTE, Image->data);
// Создаем текстуру с мип-наложением
glBindTexture(GL_TEXTURE_2D, texture[2]);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_NEAREST);
gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB8, Image->sizeX, Image->sizeY,
GL_RGB, GL_UNSIGNED_BYTE, Image->data);
}
else status=false;
if (Image) { // Если текстура существует
if (Image->data) delete Image->data; // Если изображение существует
delete Image;
Image=NULL;
}
Загрузим рельеф. По причинам, объясняемым ниже, текстура рельефа должна иметь 50% яркость, поэтому ее надо промасштабировать. Сделаем это через команды glPixelTransferf(), которые описывают попиксельное преобразование данных растра в текстуру. Если вы до сих пор не пользовались командами семейства glPixelTransfer(), рекомендую обратить на них пристальное внимание, поскольку они часто бывают очень удобны и полезны.
Теперь учтем, что нам не нужно, чтобы базовая картинка многократно повторялась в текстуре. Чтобы получить картинку единожды, растянутой в нужное количество раз, ее надо привязать к текстурным координатам с (s,t)=(0.0f, 0.0f) по (s,t)=(1.0f, 1.0f). Все остальные координаты привязываются к чистому черному цвету через вызовы glTexParameteri(), которые даже не требуют пояснений.
// Загрузим рельефы
if (Image=auxDIBImageLoad("Data/Bump.bmp")) {
glPixelTransferf(GL_RED_SCALE,0.5f); // Промасштабируем яркость до 50%,
glPixelTransferf(GL_GREEN_SCALE,0.5f); // поскольку нам нужна половинная интенсивность
glPixelTransferf(GL_BLUE_SCALE,0.5f);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP); // Не укладывать паркетом
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP);
glGenTextures(3, bump); // Создать три текстуры
// Создать текстуру с фильтром по ближайшему
>…<
// Создать текстуру с фильтром усреднения
>…<
// Создать текстуру с мип-наложением
>…<
С этой фразой вы уже знакомы: по причинам, объясненным ниже, нужно создать инвертированную карту рельефа с той же 50% яркостью. Для этого вычтем полученный ранее растр из чистого белого цвета {255, 255, 255}. Поскольку мы НЕ возвращали RGB-масштабирование на 100% уровень (я три часа разбирался, пока понял, что здесь скрывалась основная ошибка первой версии урока!), инверсный рельеф тоже получится 50% яркости.
for (int i=0; i<3*Image->sizeX*Image->sizeY; i++) // Проинвертируем растр
Image->data[i]=255-Image->data[i];
glGenTextures(3, invbump); // Создадим три текстуры
// с фильтром по ближайшему
>…<
// с фильтром усреднения
>…<
// с мип-наложением
>…<
}
else status=false;
if (Image) { // Если текстура существует
if (Image->data) delete Image->data; // Если изображение текстуры существует
delete Image;
Image=NULL;
}
Загрузка изображения логотипа очень проста, кроме, разве что, фрагмента рекомбинации RGB-A. Он, впрочем, тоже достаточно очевиден. Заметьте, что текстура строится на основе alpha-, а не Image-блока! Здесь применена только одна фильтрация.
// Загрузка картинки логотипа
if (Image=auxDIBImageLoad("Data/OpenGL_ALPHA.bmp")) {
alpha=new char[4*Image->sizeX*Image->sizeY];
// Выделим память для RGBA8-текстуры
for (int a=0; a<Image->sizeX*Image->sizeY; a++)
alpha[4*a+3]=Image->data[a*3]; // Берем красную величину как альфа-канал
if (!(Image=auxDIBImageLoad("Data/OpenGL.bmp"))) status=false;
for (a=0; a<Image->sizeX*Image->sizeY; a++) {
alpha[4*a]=Image->data[a*3]; // R
alpha[4*a+1]=Image->data[a*3+1]; // G
alpha[4*a+2]=Image->data[a*3+2]; // B
}
glGenTextures(1, &glLogo); // Создать одну текстуру
// Создать RGBA8-текстуру с фильтром усреднения
glBindTexture(GL_TEXTURE_2D, glLogo);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, Image->sizeX, Image->sizeY, 0,
GL_RGBA, GL_UNSIGNED_BYTE, alpha);
delete alpha;
}
else status=false;
if (Image) { // Если текстура существует
if (Image->data) delete Image->data; // Если изображение текстуры существует
delete Image;
Image=NULL;
}
// Загрузим логотип "Extension Enabled"
if (Image=auxDIBImageLoad("Data/multi_on_alpha.bmp")) {
alpha=new char[4*Image->sizeX*Image->sizeY]; // Выделить память для RGBA8-текстуры
>…<
glGenTextures(1, &multiLogo); // Создать одну текстуру
// Создать RGBA8-текстуру с фильтром усреднения
>…<
delete alpha;
}
else status=false;
if (Image) { // Если текстура существует
if (Image->data) delete Image->data; // Если изображение текстуры существует
delete Image;
Image=NULL;
}
return status; // Вернем состояние
}
Далее идет практически единственная неизмененная функция ReSizeGLScene(), и ее я пропустил. За ней следует функция doCube(), рисующая куб с единичными нормалями. Она задействует только текстурный блок №0, потому что glTexCoord2f(s,t) делает то же самое, что и glMultiTexCoord2f(GL_TEXTURE0_ARB,s,t). Обратите внимание, что куб нельзя создать, используя чередующиеся массивы, но это тема для отдельного разговора. Кроме того, учтите, что куб НЕВОЗМОЖНО создать, пользуясь списками отображения. Видимо, точность внутреннего представления данных, используемая в этих списках, не соответствует точности, применяемой в GLfloat. Это ведет к неприятным эффектам, которые называются проблемами деколирования (когда источник света не влияет на закрашивание объекта), поэтому от списков я решил отказаться. Вообще, я полагаю, что надо либо делать всю геометрию, пользуясь списками, либо не применять их вообще. Смешивание разных подходов приводит к проблемам, которые где-нибудь да проявятся, даже если на вашей аппаратуре все пройдет успешно.
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
// Изменить размер и инициализировать окно GL
>…<
void doCube (void) {
int i;
glBegin(GL_QUADS);
// Передняя грань
glNormal3f( 0.0f, 0.0f, +1.0f);
for (i=0; i<4; i++) {
glTexCoord2f(data[5*i],data[5*i+1]);
glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
}
// Задняя грань
glNormal3f( 0.0f, 0.0f,-1.0f);
for (i=4; i<8; i++) {
glTexCoord2f(data[5*i],data[5*i+1]);
glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
}
// Верхняя грань
glNormal3f( 0.0f, 1.0f, 0.0f);
for (i=8; i<12; i++) {
glTexCoord2f(data[5*i],data[5*i+1]);
glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
}
// Нижняя грань
glNormal3f( 0.0f,-1.0f, 0.0f);
for (i=12; i<16; i++) {
glTexCoord2f(data[5*i],data[5*i+1]);
glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
}
// Правая грань
glNormal3f( 1.0f, 0.0f, 0.0f);
for (i=16; i<20; i++) {
glTexCoord2f(data[5*i],data[5*i+1]);
glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
}
// Левая грань
glNormal3f(-1.0f, 0.0f, 0.0f);
for (i=20; i<24; i++) {
glTexCoord2f(data[5*i],data[5*i+1]);
glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
}
glEnd();
}
Время инициализировать OpenGL. Все как в Уроке 06, кроме вызова initLights() вместо прямой инициализации источников света в теле функции. Да, и еще одно: я выполняю здесь настройку мультитекстурирования.
int InitGL(GLvoid) // Все настройки OpenGL проходят здесь
{
multitextureSupported=initMultitexture();
if (!LoadGLTextures()) return false; // Переход к процедуре загрузки текстур
glEnable(GL_TEXTURE_2D); // Включить привязку текстур
glShadeModel(GL_SMOOTH); // Включит сглаживание
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Черный фон
glClearDepth(1.0f); // Установка буфера глубины
glEnable(GL_DEPTH_TEST); // Включить проверку глубины
glDepthFunc(GL_LEQUAL); // Тип проверки глубины
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Наилучшая коррекция перспективы
initLights(); // Инициализация освещения OpenGL
return true // Инициализация закончилась успешно
}
95% всей работы содержится здесь. Все, что упоминалось под грифом "по причинам, объясненным ниже", будет расписано в этом теоретическом блоке.
Начало теории ( Наложение микрорельефа методом тиснения )
Если у вас установлен просмотрщик Powerpoint-презентаций, я настоятельно рекомендую скачать следующую презентацию:
"Emboss Bump Mapping" by Michael I. Gold, nVidia Corp. [.ppt, 309K]
Для тех, у кого нет просмотрщика, я попытался перевести презентацию в html-формат. Вот она:
Наложение микрорельефа методом тиснения
Майкл И. Голд
Корпорация Nvidia
Наложение микрорельефа (bump mapping)
Действительное наложение микрорельефа использует попиксельные вычисления освещенности
Вычисление освещенности в каждой точке базируется на возмущенном векторе нормали.
Вычисления весьма ресурсоемкие.
Более детальное описание читайте здесь: Blinn, J. : Simulation of Wrinkled Surfaces (Моделирование складчатых поверхностей), Computer Graphics. 12,3 (August 1978) 286-292.
Информация в сети: на http://www.objectecture.com/ лежит Cass Everitt’s Orthogonal Illumination Thesis (Диссертация по ортогональному освещению Касса Эверитта).
Наложение микрорельефа методом тиснения (emboss bump mapping)
Микрорельеф, наложенный тиснением, похож на резьбу по материалу
Учитывается только рассеянный свет, нет зеркальной составляющей
Возможны артефакты изображения из-за недостаточного размера текстуры рельефа (в результате, например, движение приводит к сильному размытию — прим. Дженса)
Выполнение возможно на пользовательском оборудовании современного уровня (как показано — прим. Дженса)
Если рельеф выглядит хорошо, используйте его!
Расчет рассеяния света
C=(L*N) x Dl x Dm
L — вектор освещения
N — вектор нормали
Dl — цвет падающего света
Dm — цвет рассеяния материала
При наложении микрорельефа попиксельно меняется N
При наложении микрорельефа методом тиснения используется аппроксимация (L*N)
Аппроксимация коэффициента рассеяния L*N
В текстурной карте содержится поле высот
[0,1] — диапазон значений, принимаемых функцией рельефности
Первая производная определяет величину уклона m (материала) в данной точке (Заметьте, что m — чисто одномерная величина. Считайте, что m — это оценка grad(s,t) (градиента) в данной точке — прим. Дженса)
m увеличивает/уменьшает базовый коэффициент рассеяния Fd
(Fd+m) приближенно определяет (L*N) для каждого пикселя
Приближенное вычисление производной
Используется приближенное вычисление производной
Берется высота H0 в точке (s,t)
Определяется высота H1 в точке, слегка сдвинутой в направлении источника света, (s+ds,t+dt)
Исходная высота H0 вычитается из возмущенной высоты H1
Разница является оценкой мгновенного угла наклона m=H1-H0
Вычисление рельефа
1) Исходный рельеф (H0).
2) На исходный рельеф (H0) накладывается другой, (H1), слегка сдвинутый в направлении источника света.
3) Из второго вычитается первый (H0-H1). Появляются освещенные (B, bright) и затемненные (D, dark) участки.
Вычисление освещенности
Вычисляется цвет фрагмента Cf
Cf = (L*N) x Dl x Dm
(L*N) ~ (Fd + (H1-H0))
Dm x Dl закодировано в текстуре поверхности Ct.
Если хватит соображения, можно управлять Dl по отдельности (мы управляем им, пользуясь освещением OpenGL — прим. Дженса)
Cf = (Fd + (H0-H1)) x Ct
И все? Так просто!
Нет, мы еще не закончили. Мы должны:
Нарисовать текстуру (в любом графическом редакторе — прим. Дженса)
Вычислить сдвиги координат текстуры (ds,dt)
Вычислить коэффициент рассеяния Fd (управляется с помощью освещения в OpenGL — прим. Дженса)
Обе величины используют вектора нормали N и освещения L (в нашем случае явным образом вычисляются только (ds,dt) — прим. Дженса)
Теперь займемся математикой
Создание текстуры
Берегите текстуры!
В настоящее время аппаратура мультитекстурирования поддерживает максимум две текстуры! (Это утверждение устарело, но его надо иметь в виду, если хотите сохранить обратную совместимость — прим. Дженса)
Рельеф использует канал АЛЬФА (у нас это не так; но если на вашей машине карточка с чипом TNT, можете попробовать повторить предложенное здесь самостоятельно — прим. Дженса)
Максимальная высота = 1.0
Уровень нулевой высоты = 0.5
Максимальная глубина = 0.0
Цвета поверхности — каналы RGB
Внутренний формат должен быть GL_RGBA8 !!
Вычисление смещения текстур
Отображение вектора освещения в пространство нормали
Нужно получить систему координат нормали
Создадим систему координат из нормали к поверхности и вектора "вверх" (мы передаем направления texCoord генератору смещения в явном виде — прим. Дженса)
Нормаль — ось z
Перпендикулярно ей идет ось x
Направление "вверх", или ось y, получена как произведение x- и z-векторов
Построим матрицу 3x3 Mn из осей
Отобразим вектор освещения в пространстве нормали.(Mn называют также ортонормальным базисом. Можете рассматривать Mn*v как представление v в базисе, формирующем касательное пространство, а не обычное. Заметьте, что ортонормальный базис инвариантен к масштабированию, то есть при умножении векторов нормализация не теряется! — прим. Дженса)
Вычисление смещения текстур (продолжение)
Используем вектор освещения в пространстве нормали для смещения
L’ = Mn x L
Используем L’x, L’y для (ds,dt)
Используем L’z как коэффициент диффузного отражения (Совсем нет! Если вы не владелец TNT-карточки, используйте освещение OpenGL, потому что вам обязательно придется выполнять дополнительный проход — прим. Дженса)
Если вектор освещения близок к нормали, L’x и L’y малы.
Если вектор освещения близок к касательной, L’x и L’y значительны.
Что, если L’z меньше нуля?
Свет на стороне, обратной к нормали
Приравняем его вклад к нулю
Реализация на TNT
Вычисления векторов и координат текстур на хосте
Передаем коэффициент рассеяния как alpha
Можно использовать цвет вершины для передачи цвета диффузного рассеяния источника света
H0 и цвет поверхности берем из текстурного блока 0
H1 берем из текстурного блока 1 (та же самая текстура, но с другими координатами)
Используем расширение ARB_multitexture
Это расширение для комбайнов (точнее, речь идет о расширении NVIDIA_multitexture_combiners, поддерживаемом всеми карточками семейства TNT — прим. Дженса)
Реализация на TNT (продолжение)
Первичная установка комбайна 0:
(1-T0a) + T1a - 0.5 (T0a означает "текстурный блок 0, альфа-канал" — прим. Дженса)
(T1a-T0a) отображается в диапазон (-1,1), но аппаратура сжимает его до (0,1)
Смещение на 0.5 балансирует потерю от сжатия (подумайте о применении масштабирования с коэффициентом 0.5, ведь можно использовать разные карты рельефа — прим. Дженса)
Цвет диффузного рассеяния источника света можно регулировать с помощью T0c
Установка RGB комбайна 1:
(T0c * C0a + T0c * Fda - 0.5 )*2
Смещение на 0.5 балансирует потерю от сжатия
Умножение на 2 осветляет изображение
Конец теории ( Наложение микрорельефа методом тиснения )
Мы у себя делаем все не совсем так, как это предложено для TNT, поскольку хотим, чтобы наша программа работала на любом железе, однако здесь есть пара-тройка ценных идей. Во-первых, то, что на большинстве карточек наложение рельефа — многопроходная операция (хотя это не относится к семейству TNT, где рельефность можно реализовать за один двухтекстурный проход). Сейчас вы, наверное, оценили, какая отличная вещь — возможность мультитекстурирования. Теперь мы напишем 3-проходный немультитекстурный алгоритм, который можно (и мы это сделаем) реализовать за два мультитекстурных прохода.
Кроме того, вы, вероятно, поняли, что нам придется проводить умножения матриц на матрицы и матриц на вектора. Но об этом можно не беспокоиться: в OpenGL операция умножения матриц реализована (если точность правильная) и умножения матрицы на вектор реализована в функции VMatMult(M,v), где матрица M умножается на вектор v и результат сохраняется в v, то есть v:=M*v. Все передаваемые матрицы и вектора должны быть гомогенны (то бишь в одной системе координат — прим. перев.) и представлять собой матрицы 4x4 и четырехмерные вектора. Такие требования гарантируют быстрое и правильное умножение векторов и матриц по правилам OpenGL.
// Вычисляет v=vM, M — матрица 4x4 в порядке столбец-строка, v — четырехмерный вектор-строка (т.е. транспонированный)
void VMatMult(GLfloat *M, GLfloat *v) {
GLfloat res[3];
res[0]=M[ 0]*v[0]+M[ 1]*v[1]+M[ 2]*v[2]+M[ 3]*v[3];
res[1]=M[ 4]*v[0]+M[ 5]*v[1]+M[ 6]*v[2]+M[ 7]*v[3];
res[2]=M[ 8]*v[0]+M[ 9]*v[1]+M[10]*v[2]+M[11]*v[3];
v[0]=res[0];
v[1]=res[1];
v[2]=res[2];
v[3]=M[15]; // Гомогенные координаты
}
Начало теории ( Алгоритмы наложения микрорельефа методом тиснения )
Сейчас мы обсудим два разных алгоритма. Первый я нашел несколько дней назад здесь:
http://www.nvidia.com/marketing/Developer/DevRel.nsf/TechnicalDemosFrame?OpenPage
Программа называется GL_BUMP и была написана Диего Тартара (Diego Tartara) в 1999 году. Диего создал очень симпатичный пример наложения микрорельефа, хотя и не лишенный некоторых недостатков.
Однако давайте взглянем на алгоритм:
Все вектора должны быть заданы ЛИБО в координатах объекта, ЛИБО в мировых координатах.
Вычисляется вектор v направления из текущей вершины к источнику света
v нормализуется
v проецируется на касательную плоскость (Касательная плоскость — такая, которая касается поверхности в данной точке. Для нас эта точка — текущая вершина.).
(s,t) сдвигается на величины соответственно x и y координат спроецированного вектора v.
Выглядит неплохо! В основном здесь повторен алгоритм, предложенный Майклом Голдом — мы рассмотрели его в предыдущем теоретическом блоке. Однако у нового варианта есть существенный недочет: Тартара берет проекцию только в плоскости xy! Для наших целей этого недостаточно, поскольку теряется необходимая z-компонента вектора v.
Диего выполняет освещение так же, как и мы: через встроенный в OpenGL механизм расчета. Поскольку мы не можем позволить себе комбинаторный метод, предложенный Голдом (наша программа должна работать на любом оборудовании, а не только на чипах TNT!), хранить коэффициент диффузного рассеяния в альфа-канале нельзя. Вспомним, что нас в любом случае будет 3 прохода немультитекстурного / 2 прохода мультитекстурного наложения. Почему бы не применить механизм освещения из OpenGL в последнем проходе, чтобы разобраться с окружающим освещением и цветами? Правда, это возможно (и красиво выглядит) только потому, что у нас нет геометрически сложных сцен — имейте это в виду. Если, не дай Бог, возникнет нужда просчитать рельеф нескольких тысяч треугольников, придется вам изобретать что-то новое.
Далее, Диего использует мультитекстурирование, которое как мы увидим впоследствии, далеко не так просто, как может показаться для данного случая.
Вернемся к нашей реализации. Она практически совпадает с рассмотренным алгоритмом, за исключением шага проецирования, где мы используем другой подход:
Мы применяем СИСТЕМУ КООРДИНАТ ОБЪЕКТА, то есть не используем в вычислениях матрицу вида модели (modelview). Из-за этого возникает неприятный побочный эффект: если куб приходится вращать, его система координат остается неизменной, в то время как мировая система (она же система координат наблюдателя) поворачивается. Однако положение источника света не должно изменяться, то есть мировые координаты источника должны оставаться постоянными. Чтобы скомпенсировать поворот, применим широко распространенный трюк: вместо пересчета каждой вершины куба в пространство мировых координат для последующего расчета рельефа, повернем источник в том же пространстве на величину, обратную повороту куба (используем инвертированную матрицу вида модели куба). Это делается очень быстро, поскольку раз мы знаем, как матрица вида модели была создана, то можем оперативно ее инвертировать. Позже мы вернемся к этому вопросу.
Вычислим текущую вершину "c" нашей поверхности ( просто взяв ее из массива data).
Затем вычислим нормаль n длиной 1 (в военное время длина нормали может достигать четырех! :) — прим. перев.) Обычно вектор нормали известен для каждой грани куба. Это важно, так как, получая нормализованные вектора, мы уменьшаем время расчета. Определим вектор освещения v от c к источнику света l.
Осталось рассчитать матрицу Mn для получения ортонормальной проекции. Получится f.
Вычислим сдвиг текстурных координат, умножив каждый из параметров s и t на v и MAX_EMBOSS: ds = s*v*MAX_EMBOSS, dt=t*v*MAX_EMBOSS. Обратите внимание: s, t и v — вектора, а MAX_EMBOSS — нет.
Во втором проходе добавим сдвиг к текстурным координатам.
Что в модели хорошего:
Она быстрая (вычисляется только один квадратный корень и пара умножений на вершину).
Она здорово выглядит.
Работает с любыми поверхностями, не только с плоскостями.
Работает на всех акселераторах.
glBegin/glEnd-совместима: не требует "запрещенных" GL-команд.
Какие недостатки:
Модель не вполне физически корректна.
Остаются мелкие артефакты.
На этом рисунке показано, где расположены вектора. t и s можно получить путем вычитания смежных векторов, но нужно следить за тем, чтобы они были верно направлены и нормализованы. Синей точкой помечена вершина, к которой проведена привязка texCoord2f(0.0f,0.0f).
Конец теории ( Алгоритмы наложения микрорельефа методом тиснения )
Давайте сначала рассмотрим формирование сдвига текстурных координат. Функция называется SetUpBumps(), потому что именно этим она и занимается:
// Выполнение сдвига текстуры
// n : нормаль к поверхности. Должна иметь длину 1
// c : текущая вершина на поверхности (координаты местоположения)
// l : положение источника света
// s : направление s-координаты текстуры в пространстве объекта
// (должна быть нормализована!)
// t : направление t-координаты текстуры в пространстве объекта
// (должна быть нормализована!)
void SetUpBumps(GLfloat *n, GLfloat *c, GLfloat *l, GLfloat *s, GLfloat *t) {
GLfloat v[3]; // Вектор от текущей точки к свету
GLfloat lenQ; // Используется для нормализации
// Вычислим и нормализуем v
v[0]=l[0]-c[0];
v[1]=l[1]-c[1];
v[2]=l[2]-c[2];
lenQ=(GLfloat) sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
v[0]/=lenQ;
v[1]/=lenQ;
v[2]/=lenQ;
// Получим величины проекции v вдоль каждой оси системы текстурных координат
c[0]=(s[0]*v[0]+s[1]*v[1]+s[2]*v[2])*MAX_EMBOSS;
c[1]=(t[0]*v[0]+t[1]*v[1]+t[2]*v[2])*MAX_EMBOSS;
}
Не так уж все и сложно, а? Но знание теории необходимо для понимания и управления эффектом (я даже сам разобрался в ЭТОМ, пока писал урок).
Мне нравится, чтобы во время работы презентационных программ по экрану летал логотип. У нас их целых два. Вызов doLogo() сбрасывает матрицу GL_MODELVIEW, поэтому он будет выполнен на последней стадии визуализации.
Функция отображает два логотипа: OpenGL и логотип мультитекстурного режима, если он включен. Логотипы содержат альфа-канал и, соответственно, полупрозрачны. Для реализации этого эффекта использованы GL_SRC_ALPHA и GL_ONE_MINUS_SRC_ALPHA, как рекомендовано OpenGL-документацией. Логотипы планарны, поэтому проводить z-сортировку нет необходимости. Числа, взятые для их высот, подобраны эмпирически (a.k.a. методом научного тыка) так, чтобы все помещалось в края экрана. Нужно включить смешивание и выключить освещение, чтобы избежать неприятных эффектов, а чтобы гарантировать размещение логотипов поверх сцены, достаточно сбросить матрицу GL_MODELVIEW и установить функцию глубины в GL_ALWAYS.
void doLogo(void) {
// ВЫЗЫВАТЬ В ПОСЛЕДНЮЮ ОЧЕРЕДЬ!!! отображает два логотипа
glDepthFunc(GL_ALWAYS);
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
glEnable(GL_BLEND);
glDisable(GL_LIGHTING);
glLoadIdentity();
glBindTexture(GL_TEXTURE_2D,glLogo);
glBegin(GL_QUADS);
glTexCoord2f(0.0f,0.0f); glVertex3f(0.23f, -0.4f,-1.0f);
glTexCoord2f(1.0f,0.0f); glVertex3f(0.53f, -0.4f,-1.0f);
glTexCoord2f(1.0f,1.0f); glVertex3f(0.53f, -0.25f,-1.0f);
glTexCoord2f(0.0f,1.0f); glVertex3f(0.23f, -0.25f,-1.0f);
glEnd();
if (useMultitexture) {
glBindTexture(GL_TEXTURE_2D,multiLogo);
glBegin(GL_QUADS);
glTexCoord2f(0.0f,0.0f); glVertex3f(-0.53f, -0.25f,-1.0f);
glTexCoord2f(1.0f,0.0f); glVertex3f(-0.33f, -0.25f,-1.0f);
glTexCoord2f(1.0f,1.0f); glVertex3f(-0.33f, -0.15f,-1.0f);
glTexCoord2f(0.0f,1.0f); glVertex3f(-0.53f, -0.15f,-1.0f);
glEnd();
}
}
Здесь начинается функция, реализующая наложение микрорельефа без использования мультитекстурирования. Это трехпроходная реализация. На первом шаге GL_MODELVIEW инвертируется путем применения к тождественной ей матрице всех шагов, применяемых позже к GL_MODELVIEW, но в обратном порядке и с инвертированными величинами. Такая матрица преобразования, будучи применена к объекту, "отменяет" воздействие GL_MODELVIEW. Мы получим ее от OpenGL вызовом glGetFloatv(). Напоминаю, что матрица должна быть массивом из 16 величин и что она транспонирована!
Кстати: если вы не уверены, в каком порядке была создана матрица вида модели, подумайте о возможности использования мировой системы координат, потому что классическая инверсия произвольной матрицы — вычислительно очень дорогостоящая операция. Впрочем, при обработке значительного числа вершин инверсия матрицы вида модели может быть более приемлемым выходом и, возможно, будет выполняться быстрее, чем расчет мировых координат для каждой вершины.
bool doMesh1TexelUnits(void) {
GLfloat c[4]={0.0f,0.0f,0.0f,1.0f}; // Текущая вершина
GLfloat n[4]={0.0f,0.0f,0.0f,1.0f}; // Нормаль к текущей поверхности
GLfloat s[4]={0.0f,0.0f,0.0f,1.0f}; // s-вектор, нормализованный
GLfloat t[4]={0.0f,0.0f,0.0f,1.0f}; // t-вектор, нормализованный
GLfloat l[4]; // Содержит координаты источника освещения,
// который будет переведен в мировые координаты
GLfloat Minv[16]; // Инвертированная матрица вида модели
int i;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экрана и буфера глубины
// Инвертируем матрицу вида модели. Заменяет один Push/Pop и один glLoadIdentity();
// Выполняется проведением всех преобразований в обратную сторону в обратном порядке
glLoadIdentity();
glRotatef(-yrot,0.0f,1.0f,0.0f);
glRotatef(-xrot,1.0f,0.0f,0.0f);
glTranslatef(0.0f,0.0f,-z);
glGetFloatv(GL_MODELVIEW_MATRIX,Minv);
glLoadIdentity();
glTranslatef(0.0f,0.0f,z);
glRotatef(xrot,1.0f,0.0f,0.0f);
glRotatef(yrot,0.0f,1.0f,0.0f);
// Преобразование положения источника в систему координат объекта:
l[0]=LightPosition[0];
l[1]=LightPosition[1];
l[2]=LightPosition[2];
l[3]=1.0f; // Гомогенные координаты
VMatMult(Minv,l);
На первом шаге надо:
Использовать текстуру рельефа
Отключить смешивание
Отключить освещение
Использовать несмещенные текстурные координаты
Построить геометрию
Будет визуализирован куб, содержащий только текстуру рельефа.
glBindTexture(GL_TEXTURE_2D, bump[filter]);
glDisable(GL_BLEND);
glDisable(GL_LIGHTING);
doCube();
На втором шаге надо:
Использовать инвертированную текстуру рельефа
Включить смешивание GL_ONE, GL_ONE
Освещение остается отключенным
Использовать смещенные координаты текстуры (это значит, что перед просчетом каждой грани куба придется вызывать SetUpBumps()).
Построить геометрию
Здесь будет визуализирован куб с корректно наложенной картой высот, но без цветов.
Можно было бы уменьшить время вычисления, повернув вектор освещения в обратную сторону. Однако этот способ не работает корректно, так что мы сделаем все просто: повернем каждую нормаль и центр так же, как делаем это с геометрией.
glBindTexture(GL_TEXTURE_2D,invbump[filter]);
glBlendFunc(GL_ONE,GL_ONE);
glDepthFunc(GL_LEQUAL);
glEnable(GL_BLEND);
glBegin(GL_QUADS);
// Передняя грань
n[0]=0.0f;
n[1]=0.0f;
n[2]=1.0f;
s[0]=1.0f;
s[1]=0.0f;
s[2]=0.0f;
t[0]=0.0f;
t[1]=1.0f;
t[2]=0.0f;
for (i=0; i<4; i++) {
c[0]=data[5*i+2];
c[1]=data[5*i+3];
c[2]=data[5*i+4];
SetUpBumps(n,c,l,s,t);
glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
}
// Задняя грань
n[0]=0.0f;
n[1]=0.0f;
n[2]=-1.0f;
s[0]=-1.0f;
s[1]=0.0f;
s[2]=0.0f;
t[0]=0.0f;
t[1]=1.0f;
t[2]=0.0f;
for (i=4; i<8; i++) {
c[0]=data[5*i+2];
c[1]=data[5*i+3];
c[2]=data[5*i+4];
SetUpBumps(n,c,l,s,t);
glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
}
// Верхняя грань
n[0]=0.0f;
n[1]=1.0f;
n[2]=0.0f;
s[0]=1.0f;
s[1]=0.0f;
s[2]=0.0f;
t[0]=0.0f;
t[1]=0.0f;
t[2]=-1.0f;
for (i=8; i<12; i++) {
c[0]=data[5*i+2];
c[1]=data[5*i+3];
c[2]=data[5*i+4];
SetUpBumps(n,c,l,s,t);
glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
}
// Нижняя грань
n[0]=0.0f;
n[1]=-1.0f;
n[2]=0.0f;
s[0]=-1.0f;
s[1]=0.0f;
s[2]=0.0f;
t[0]=0.0f;
t[1]=0.0f;
t[2]=-1.0f;
for (i=12; i<16; i++) {
c[0]=data[5*i+2];
c[1]=data[5*i+3];
c[2]=data[5*i+4];
SetUpBumps(n,c,l,s,t);
glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
}
// Правая грань
n[0]=1.0f;
n[1]=0.0f;
n[2]=0.0f;
s[0]=0.0f;
s[1]=0.0f;
s[2]=-1.0f;
t[0]=0.0f;
t[1]=1.0f;
t[2]=0.0f;
for (i=16; i<20; i++) {
c[0]=data[5*i+2];
c[1]=data[5*i+3];
c[2]=data[5*i+4];
SetUpBumps(n,c,l,s,t);
glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
}
// Левая грань
n[0]=-1.0f;
n[1]=0.0f;
n[2]=0.0f;
s[0]=0.0f;
s[1]=0.0f;
s[2]=1.0f;
t[0]=0.0f;
t[1]=1.0f;
t[2]=0.0f;
for (i=20; i<24; i++) {
c[0]=data[5*i+2];
c[1]=data[5*i+3];
c[2]=data[5*i+4];
SetUpBumps(n,c,l,s,t);
glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
}
glEnd();
На третьем шаге надо:
Использовать основную (цветную) текстуру
Включить смешивание GL_DST_COLOR, GL_SRC_COLOR
Уравнения смешивания фактически получает множитель 2: (Cdst*Csrc)+(Csrc*Cdst)=2(Csrc*Cdst)!
Включить освещение для расчета фонового и диффузного освещения
Сбросить матрицу GL_TEXTURE с целью вернуться к "нормальным" текстурным координатам
Построить геометрию
Это заключительная стадия расчета, с учетом освещения. Чтобы корректно переключаться между мультитекстурным и однотекстурным режимами, надо сначала выставить среду текстурирования в "нормальный" режим GL_MODULATE. Если захочется отказаться от наложения цветной текстуры, достаточно ограничиться первыми двумя проходами и пропустить третий.
if (!emboss) {
glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glBindTexture(GL_TEXTURE_2D,texture[filter]);
glBlendFunc(GL_DST_COLOR,GL_SRC_COLOR);
glEnable(GL_LIGHTING);
doCube();
}
На финальном шаге надо:
Обновить геометрию (особенно вращение)
Отобразить логотипы
xrot+=xspeed;
yrot+=yspeed;
if (xrot>360.0f) xrot-=360.0f;
if (xrot<0.0f) xrot+=360.0f;
if (yrot>360.0f) yrot-=360.0f;
if (yrot<0.0f) yrot+=360.0f;
/* ПОСЛЕДНИЙ ПРОХОД: Даешь логотипы! */
doLogo();
return true; // Продолжаем
}
Следующая новая функция выполнит всю задачу за 2 прохода с использованием мультитекстурирования. Будут задействованы два текстурных блока, большее их количество резко усложнит уравнения смешивания. Лучше уж заниматься оптимизацией под TNT. Обратите внимание, практически единственное отличие от doMesh1TexelUnits() в том, что для каждой вершины отсылается не один, а два набора текстурных координат.
bool doMesh2TexelUnits(void) {
GLfloat c[4]={0.0f,0.0f,0.0f,1.0f}; // Здесь храним текущую вершину
GLfloat n[4]={0.0f,0.0f,0.0f,1.0f}; // Вектор нормали к текущей поверхности
GLfloat s[4]={0.0f,0.0f,0.0f,1.0f}; // s-вектор, нормализованный
GLfloat t[4]={0.0f,0.0f,0.0f,1.0f}; // t-вектор, нормализованный
GLfloat l[4]; // Хранит координаты источника света,
// для перевода в пространство координат объекта
GLfloat Minv[16]; // Инвертированная матрица вида модели
int i;
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистим экран и буфер глубины
// Инвертируем матрицу вида модели. Заменяет один Push/Pop и один glLoadIdentity();
// Выполняется проведением всех преобразований в обратную сторону в обратном порядке
glLoadIdentity();
glRotatef(-yrot,0.0f,1.0f,0.0f);
glRotatef(-xrot,1.0f,0.0f,0.0f);
glTranslatef(0.0f,0.0f,-z);
glGetFloatv(GL_MODELVIEW_MATRIX,Minv);
glLoadIdentity();
glTranslatef(0.0f,0.0f,z);
glRotatef(xrot,1.0f,0.0f,0.0f);
glRotatef(yrot,0.0f,1.0f,0.0f);
// Преобразуем координаты источника света в систему координат объекта
l[0]=LightPosition[0];
l[1]=LightPosition[1];
l[2]=LightPosition[2];
l[3]=1.0f; // Гомогенные координаты
VMatMult(Minv,l);
На первом шаге надо:
Отменить смешивание
Отменить освещение
Установить текстурный комбайн 0 на
Использование текстуры рельефа
Использование несмещенных координат текстуры
Выполнение операции GL_REPLACE, то есть простое отображение текстуры
Установить текстурный комбайн 1 на
Использование сдвинутых текстурных координат
Выполнение операции GL_ADD, эквивалента однотекстурного ONE-ONE-смешивания.
Будет рассчитан куб с наложенной картой эрозии поверхности.
// ТЕКСТУРНЫЙ БЛОК #0
glActiveTextureARB(GL_TEXTURE0_ARB);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, bump[filter]);
glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_EXT);
glTexEnvf (GL_TEXTURE_ENV, GL_COMBINE_RGB_EXT, GL_REPLACE);
// ТЕКСТУРНЫЙ БЛОК #1
glActiveTextureARB(GL_TEXTURE1_ARB);
glEnable(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, invbump[filter]);
glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_EXT);
glTexEnvf (GL_TEXTURE_ENV, GL_COMBINE_RGB_EXT, GL_ADD);
// Общие флаги
glDisable(GL_BLEND);
glDisable(GL_LIGHTING);
Теперь визуализируем грани одну за одной, как это было сделано в doMesh1TexelUnits(). Единственное отличие — вместо glTexCoord2f() используется glMultiTexCoor2fARB(). Обратите внимание, надо прямо указывать, какой текстурный блок вы имеете в виду. Параметр должен иметь вид GL_TEXTUREi_ARB, где i лежит в диапазоне [0..31]. ( Это что же за карточка с 32 текстурными блоками? И зачем она?)
glBegin(GL_QUADS);
// Передняя грань
n[0]=0.0f;
n[1]=0.0f;
n[2]=1.0f;
s[0]=1.0f;
s[1]=0.0f;
s[2]=0.0f;
t[0]=0.0f;
t[1]=1.0f;
t[2]=0.0f;
for (i=0; i<4; i++) {
c[0]=data[5*i+2];
c[1]=data[5*i+3];
c[2]=data[5*i+4];
SetUpBumps(n,c,l,s,t);
glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
}
// Задняя грань
n[0]=0.0f;
n[1]=0.0f;
n[2]=-1.0f;
s[0]=-1.0f;
s[1]=0.0f;
s[2]=0.0f;
t[0]=0.0f;
t[1]=1.0f;
t[2]=0.0f;
for (i=4; i<8; i++) {
c[0]=data[5*i+2];
c[1]=data[5*i+3];
c[2]=data[5*i+4];
SetUpBumps(n,c,l,s,t);
glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
}
// Верхняя грань
n[0]=0.0f;
n[1]=1.0f;
n[2]=0.0f;
s[0]=1.0f;
s[1]=0.0f;
s[2]=0.0f;
t[0]=0.0f;
t[1]=0.0f;
t[2]=-1.0f;
for (i=8; i<12; i++) {
c[0]=data[5*i+2];
c[1]=data[5*i+3];
c[2]=data[5*i+4];
SetUpBumps(n,c,l,s,t);
glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
}
// Нижняя грань
n[0]=0.0f;
n[1]=-1.0f;
n[2]=0.0f;
s[0]=-1.0f;
s[1]=0.0f;
s[2]=0.0f;
t[0]=0.0f;
t[1]=0.0f;
t[2]=-1.0f;
for (i=12; i<16; i++) {
c[0]=data[5*i+2];
c[1]=data[5*i+3];
c[2]=data[5*i+4];
SetUpBumps(n,c,l,s,t);
glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
}
// Правая грань
n[0]=1.0f;
n[1]=0.0f;
n[2]=0.0f;
s[0]=0.0f;
s[1]=0.0f;
s[2]=-1.0f;
t[0]=0.0f;
t[1]=1.0f;
t[2]=0.0f;
for (i=16; i<20; i++) {
c[0]=data[5*i+2];
c[1]=data[5*i+3];
c[2]=data[5*i+4];
SetUpBumps(n,c,l,s,t);
glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
}
// Левая грань
n[0]=-1.0f;
n[1]=0.0f;
n[2]=0.0f;
s[0]=0.0f;
s[1]=0.0f;
s[2]=1.0f;
t[0]=0.0f;
t[1]=1.0f;
t[2]=0.0f;
for (i=20; i<24; i++) {
c[0]=data[5*i+2];
c[1]=data[5*i+3];
c[2]=data[5*i+4];
SetUpBumps(n,c,l,s,t);
glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
}
glEnd();
На втором шаге надо:
Использовать основную текстуру
Включить освещение
Отменить смещение текстурных координат => сброc матрицы GL_TEXTURE
Текстурную среду вернуть в состояние GL_MODULATE, чтобы заработало освещение OpenGL (иначе не получится!)
Здесь уже будет полностью готов куб.
glActiveTextureARB(GL_TEXTURE1_ARB);
glDisable(GL_TEXTURE_2D);
glActiveTextureARB(GL_TEXTURE0_ARB);
if (!emboss) {
glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glBindTexture(GL_TEXTURE_2D,texture[filter]);
glBlendFunc(GL_DST_COLOR,GL_SRC_COLOR);
glEnable(GL_BLEND);
glEnable(GL_LIGHTING);
doCube();
}
На последнем шаге надо
Обновить геометрию (особенно вращение)
Отобразить логотипы
xrot+=xspeed;
yrot+=yspeed;
if (xrot>360.0f) xrot-=360.0f;
if (xrot<0.0f) xrot+=360.0f;
if (yrot>360.0f) yrot-=360.0f;
if (yrot<0.0f) yrot+=360.0f;
/* ПОСЛЕДНИЙ ПРОХОД: да будут логотипы! */
doLogo();
return true; // Продолжим
}
И, для сравнения, функция, рисующая куб без рельефа — почувствуйте разницу!
bool doMeshNoBumps(void) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистить экран и буфер глубины
glLoadIdentity(); // Сбросить вид
glTranslatef(0.0f,0.0f,z);
glRotatef(xrot,1.0f,0.0f,0.0f);
glRotatef(yrot,0.0f,1.0f,0.0f);
if (useMultitexture) {
glActiveTextureARB(GL_TEXTURE1_ARB);
glDisable(GL_TEXTURE_2D);
glActiveTextureARB(GL_TEXTURE0_ARB);
}
glDisable(GL_BLEND);
glBindTexture(GL_TEXTURE_2D,texture[filter]);
glBlendFunc(GL_DST_COLOR,GL_SRC_COLOR);
glEnable(GL_LIGHTING);
doCube();
xrot+=xspeed;
yrot+=yspeed;
if (xrot>360.0f) xrot-=360.0f;
if (xrot<0.0f) xrot+=360.0f;
if (yrot>360.0f) yrot-=360.0f;
if (yrot<0.0f) yrot+=360.0f;
/* ПОСЛЕДНИЙ ПРОХОД: логотипы */
doLogo();
return true; // Продолжим
}
Все, что должна делать функция drawGLScene() — определить, какой из вариантов doMesh вызвать:
bool DrawGLScene(GLvoid) // Здесь все рисуется
{
if (bumps) {
if (useMultitexture && maxTexelUnits>1)
return doMesh2TexelUnits();
else return doMesh1TexelUnits();
}
else return doMeshNoBumps();
}
Убиваем GLWindow. Функция не изменялась, а потому не приведена:
GLvoid KillGLWindow(GLvoid) // Уничтожим окно корректно
>…<
Функция создает GLWindow; не изменена, поэтому пропущена:
BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
>…<
Основной цикл обработки сообщений; не изменен, поэтому пропущен:
LRESULT CALLBACK WndProc( HWND hWnd, // Указатель окна
UINT uMsg, // Сообщение для этого окна
WPARAM wParam, // Дополнительная информация о сообщении
LPARAM lParam) // Дополнительная информация о сообщении
>…<
Основная функция окна. Здесь добавлена обработка различных дополнительных кнопок:
E: Переключает режимы чистого рельефа / рельефа с текстурой
M: Включает/отключает мультитекстурирование
B: Включает/отключает наложение микрорельефа. Опция является взаимоисключающей с настройками, управляемыми кнопкой E
F: Переключает способы фильтрации. Вы увидите, насколько режим GL_NEAREST не создан для рельефного текстурирования
Клавиши управления курсором: Вращение куба
int WINAPI WinMain( HINSTANCE hInstance, // Экземпляр приложения
HINSTANCE hPrevInstance, // Предыдущий экземпляр
LPSTR lpCmdLine, // Параметры командной строки
int nCmdShow) // Показать состояние окна
{
>…<
if (keys['E'])
{
keys['E']=false;
emboss=!emboss;
}
if (keys['M'])
{
keys['M']=false;
useMultitexture=((!useMultitexture) && multitextureSupported);
}
if (keys['B'])
{
keys['B']=false;
bumps=!bumps;
}
if (keys['F'])
{
keys['F']=false;
filter++;
filter%=3;
}
if (keys[VK_PRIOR])
{
z-=0.02f;
}
if (keys[VK_NEXT])
{
z+=0.02f;
}
if (keys[VK_UP])
{
xspeed-=0.01f;
}
if (keys[VK_DOWN])
{
xspeed+=0.01f;
}
if (keys[VK_RIGHT])
{
yspeed+=0.01f;
}
if (keys[VK_LEFT])
{
yspeed-=0.01f;
}
}
}
}
// Выключаемся
KillGLWindow(); // Убить окно
return (msg.wParam); // Выйти из программы
}
Еще несколько слов о генерации текстур и наложении рельефа на объекты, прежде чем вы начнете ваять великие игры и поражаться, почему они идут так медленно и так ужасно выглядят:
Не стоит использовать текстуры рельефа размером 256x256, как в этом уроке. Все начинает сильно тормозить, поэтому такие размеры подходят только для демонстрационных целей (например, в уроках).
Куб, имеющий рельеф — редкая вещь. Повернутый рельефный куб — и того реже. Причина в том, что угол зрения сильно влияет на качество изображения, и чем он больше, тем хуже результат. Практически все многопроходные алгоритмы подвержены этому недостатку. Чтобы не применять текстуры высокого разрешения, увеличьте минимальный угол зрения до приемлемой величины или уменьшите диапазон углов и проводите предварительную фильтрацию текстур так, чтобы удовлетворить этому диапазону.
Сначала создайте основную текстуру. Рельеф можно сделать позже в любом редакторе, переведя картинку в шкалу серого.
Рельеф должен быть "острее" и контрастнее основной текстуры. Это можно сделать, применив фильтр "резкость" (sharpen) к основной текстуре. Поначалу может смотреться странно, но поверьте: чтобы получить первоклассный оптический эффект, нужно КАПИТАЛЬНО "заострить" текстуру.
Текстура рельефа должна быть отцентрована по 50% серому, (RGB=127,127,127), поскольку это эквивалентно отсутствию рельефа. Более яркие значения соответствуют выпуклостям, а менее яркие — провалам. Результат можно оценить, просмотрев текстуру в режиме гистограммы в каком-нибудь подходящем редакторе.
Текстура рельефа может быть в четверть размера основной текстуры, и это не приведет к катастрофическим последствиям, хотя разница, конечно, будет заметна.
Теперь у вас должно быть некоторое представление о вещах, обсужденных в этом уроке. Надеюсь, вам понравилось.
Вопросы, пожелания, предложения, просьбы, жалобы? Почтуйте, потому что веб-странички у меня пока еще нет.
Это мой основной проект; надеюсь, вскоре последует продолжение.
Моя признательность:
Michael I. Gold за документацию по наложению микрорельефа
Diego Tartara за код этого примера
NVidia за размещение отличных примеров в Сети
И, конечно, NeHe за неоценимую помощь в освоении OpenGL.
Лексемы, Расширения, Вырезка и Загрузка TGA
Tokens, Extensions, Scissor Testing And TGA Loading
Этот урок далеко не идеален, но Вы определенно узнаете кое-что новое. Я получил довольно много вопросов о расширениях и о том, как определить какие расширения поддерживаются конкретным типом видеокарты. Этот урок научит Вас определять, какие 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 для использования их в качестве текстур.
Если у Вас возникли какие-либо проблемы с этим уроком, или Вы находите, что эта информация трудна для понимания, дайте мне знать. Я хочу сделать свои уроки лучше, насколько это возможно. Обратная связь очень важна!