Урок 22. Наложение микрорельефа методом тиснения, мультитекстурирование и использование расширений OpenGL
Этот урок, написанный Дженсом Шнайдером (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.
© Jens Schneider
Jeff Molofee (NeHe)
5 ноября 2002 (c) Vasily Chernikov