Реалистичное отражение с использование буфера шаблона и отсечения.
Clipping & Reflections Using The Stencil Buffer
Добро пожаловать в следующий урок № 26, довольно интересный урок. Код для него написал Banu Cosmin. Автором, конечно же, являюсь я (NeHe). Здесь вы научитесь создавать исключительно реалистичные отражения безо всяких подделок. Отражаемые объекты не будут выглядеть как бы под полом или по другую сторону стены, нет. Отражение будет настоящим!
Очень важное замечание по теме этого занятия: поскольку Voodoo1, 2 и некоторые другие видеокарты не поддерживают буфер шаблона (stencil buffer, или буфер трафарета или стенсильный буфер), программа на них не заработает. Код, приведенный здесь, работает ТОЛЬКО на картах, поддерживающих буфер шаблона. Если вы не уверены, что ваша видеосистема его поддерживает, скачайте пример и попробуйте его запустить. Для программы не требуется мощный процессор и видеокарта. Даже на моем GeForce1 лишь временами я замечал некоторое замедление. Данная демонстрационная программа лучше всего работает в 32-битном цветовом режиме.
Поскольку видеокарты становятся все лучше, процессоры - быстрее, с моей точки зрения, поддержка буфера шаблона становится все более распространенной. Итак, если оборудование позволяет и вы готовы к отражению, приступим к занятию!
Первая часть программы довольно стандартна. Мы включаем необходимые заголовочные файлы, готовим наш контекст устройства (Device Context), контекст рендеринга (Rendering Context) и т.д.
#include <windows.h> // заголовочный файл для Windows
#include <math.h> // заголовочный файл для математической библиотеки Windows(добавлено)
#include <stdio.h> // заголовочный файл для стандартного ввода/вывода(добавлено)
#include <stdarg.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; // Копия нашего приложения
Далее идут стандартные переменные: keys[] – массив для работы с последовательностями нажатий клавиш, active – показывает, активна программа или нет, и индикатор полноэкранного режима – fullscreen.
bool keys[256]; // Массив для работы с клавиатурой
bool active=TRUE; // Флаг активности окна, TRUE по умолчанию
bool fullscreen=TRUE; // Флаг полноэкранного режима, TRUE по умолчанию
Далее мы настроим параметры нашего освещения. В LightAmb[] поместим настройки окружающего освещения. Возьмем его составляющие в пропорции 70% красного, 70% синего и 70% зеленого цвета, что даст нам белый свет с яркостью в 70%. LightDif[]-массив с настройками рассеянного освещения (это составляющая света, равномерно отражаемая поверхностью нашего объекта). В данном случае нам потребуется освещение максимальной интенсивности для наилучшего отражения. И массив LightPos[] используется для размещения источника освещения. Сместим его на 4 единицы вправо, 4 единицы вверх и на 6 единиц к наблюдателю. Для более актуального освещения источник размещается на переднем плане правого верхнего угла экрана.
//Параметры освещения
static Glfloat LightAmb[]={0.7f, 0.7f, 0.7f}; //Окружающий свет
static Glfloat LightDif[]={1.0f, 1.0f, 1.0f}; //Рассеянный свет
//Позиция источника освещения
static Glfloat LightPos[]={4.0f, 4.0f, 6.0f, 1.0f};
Настроим переменную q для нашего квадратичного объекта (квадратичным объект, по-видимому, называется по той причине, что его полигонами являются прямоугольники – прим.перев), xrot и yrot – для осуществления вращения. xrotspeed и yrotspeed – для управления скоростью вращения. Переменная zoom используется для приближения и отдаления нашей сцены (начальное значение = -7, при котором мы увидим полную сцену), и переменная height содержит значение высоты мяча над полом.
Затем выделим массив для трех текстур и определим WndProc().
GLUquadricObj *q; // Квадратичный объект для рисования сферы мяча
GLfloat xrot = 0.0f; // Вращение по Х
GLfloat yrot = 0.0f; // Вращение по Y
GLfloat xrotspeed = 0.0f;// Скорость вращения по X
GLfloat yrotspeed = 0.0f;// Скорость вращения по Y
GLfloat zoom = -7.0f; // Глубина сцены в экране
GLfloat height = 2.0f; // Высота мяча над полом
GLuint texture[3]; // 3 Текстуры
// Объявление WndProc
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
Функции ReSizeGLScene() и LoadBMP() не меняются, так что я их обе пропускаю.
// Функция изменения размера и инициализации OpenGL-окна
GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
// Функция загрузки растрового рисунка
AUX_RGBImageRec *LoadBMP(char *Filename)
Код, загружающий текстуру довольно стандартный. Вы могли пользоваться им не раз при изучении предыдущих статей. Мы создали массив для трех текстур, затем мы загружаем три рисунка и создаем три текстуры с линеарной фильтрацией из данных рисунков. Файлы с растровыми рисунками мы ищем в каталоге DATA.
int LoadGLTextures() // Загрузка рисунков и создание текстур
{
int Status=FALSE; // Индикатор статуса
AUX_RGBImageRec *TextureImage[3]; // массив для текстур
memset(TextureImage,0,sizeof(void *)*3); // Обнуление
if ((TextureImage[0]=LoadBMP("Data/EnvWall.bmp")) && // Текстура пола
(TextureImage[1]=LoadBMP("Data/Ball.bmp")) && // Текстура света
(TextureImage[2]=LoadBMP("Data/EnvRoll.bmp"))) // Текстура стены
{
Status=TRUE; // Статус положительный
glGenTextures(3, &texture[0]); // Создание текстуры
for (int loop=0; loop<3; loop++) // Цикл для 3 текстур
{
glBindTexture(GL_TEXTURE_2D, texture[loop]);
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop]->sizeX,
TextureImage[loop]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE,
TextureImage[loop]->data);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
}
for (loop=0; loop<3; loop++) // Цикл для 3 рисунков
{
if (TextureImage[loop]) // Если текстура существует
{
if (TextureImage[loop]->data) // Если рисунок есть
{
free(TextureImage[loop]->data);
}
free(TextureImage[loop]); // Очистить память из-под него
}
}
}
return Status; // Вернуть статус
}
В функции инициализации представлена новая команда OpenGL – glClearStencil. Значение параметра = 0 для этой команды говорит о том, что очищать буфер шаблона не надо. С остальной частью функции вы уже, должно быть, знакомы. Мы загружаем наши текстуры и включаем плавное закрашивание. Цвет очистки экрана - синий, значение очистки буфера глубины = 1.0f. Значение очистки буфера шаблона = 0. Мы включаем проверку глубины и устанавливаем значение проверки глубины меньше или равной установленному значению. Коррекция перспективы выбрана наилучшего качества и включается 2D-текстурирование.
int InitGL(GLvoid) // Инициализация OpenGL
{
if (!LoadGLTextures()) // Если текстуры не загружены, выход
{
return FALSE;
}
glShadeModel(GL_SMOOTH);//Включаем плавное закрашивание
glClearColor(0.2f, 0.5f, 1.0f, 1.0f);// Фон
glClearDepth(1.0f); // Значение для буфера глубины
glClearStencil(0); // Очистка буфера шаблона 0
glEnable(GL_DEPTH_TEST);//Включить проверку глубины
glDepthFunc(GL_LEQUAL); // Тип проверки глубины
// Наилучшая коррекция перспективы
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
glEnable(GL_TEXTURE_2D);//Включить рисование 2D-текстур
Теперь пора настроить источник света GL_LIGHT0. Первая нижеприведенная строка говорит OpenGL использовать массив LightAmp для окружающего света. Если вы помните начало программы, RGB-компоненты в этом массиве были все равны 0.7f , что означает 70% интенсивности белого света. Затем мы при помощи массива LightDif настраиваем рассеянный свет, и местоположение источника света – значениями x,y,z из массива LightPos.
После настройки света мы включаем его командой glEnable(GL_LIGHT0). Хотя источник включен, вы его не увидите, пока не включите освещение командой glEnable(GL_LIGHTING).
Примечание: если вы хотите отключить все источники света в сцене, примените команду glDisable(GL_LIGHTING). Для отключения определенного источника света надо использовать команду glEnable(GL_LIGHT{0-7}). Эти команды позволяют нам контролировать освещение в целом, а также источники по отдельности. Еще раз запомните, пока не отработает команда glEnable(GL_LIGHTING), ничего вы на своей 3D-сцене не увидите.
// Фоновое освещение для источника LIGHT0
glLightfv(GL_LIGHT0, GL_AMBIENT, LightAmb);
// Рассеянное освещение для источника LIGHT0
glLightfv(GL_LIGHT0, GL_DIFFUSE, LightDif);
// Положение источника LIGHT0
glLightfv(GL_LIGHT0, GL_POSITION, LightPos);
// Включить Light0
glEnable(GL_LIGHT0);
// Включить освещение
glEnable(GL_LIGHTING);
В первой строке мы создаем новый квадратичный объект. Затем мы говорим OpenGL о типе генерируемых нормалей для нашего квадратичного объекта - нормали сглаживания. Третья строка включает генерацию координат текстуры для квадратичного объекта. Без этих строк – второй и третьей закрашивание объекта будет плоским и невозможно будет наложить на него текстуру.
Четвертая и пятая строки говорят OpenGL использовать алгоритм сферического наложения (Sphere Mapping) для генерации координат для текстуры. Это дает нам доступ к сферической поверхности квадратичного объекта.
q = gluNewQuadric(); // Создать квадратичный объект
// тип генерируемых нормалей для него – «сглаженные»
gluQuadricNormals(q, GL_SMOOTH);
// Включить текстурные координаты для объекта
gluQuadricTexture(q, GL_TRUE);
// Настройка сферического наложения
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
// Настройка отображения сферы
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
return TRUE; // Инициализация прошла успешно
}
Нижеприведенная функция рисует наш объект (в самом деле, неплохо смотрящийся пляжный мяч).
Устанавливаем цвет с максимальной интенсивностью белого и подключаем текстуру мяча (состоящую из последовательности красной, белой и синей полос).
После этого мы рисуем квадратичную сферу (Quadratic Sphere) с радиусом в 0.35f, 32 срезами (разбиениями вокруг оси Z) и 16 полосами (разбиениями вдоль оси Z) (вверх и вниз).
void DrawObject() // Рисование мяча
{
glColor3f(1.0f, 1.0f, 1.0f);// Цвет - белый
glBindTexture(GL_TEXTURE_2D, texture[1]);// Выбор текстуры 2 (1)
gluSphere(q, 0.35f, 32, 16);// Рисование первого мяча
Нарисовав первый мяч, выбираем новую текстуру (EnvRoll), устанавливаем значение прозрачности в 40% и разрешаем смешивание цветов, основанное на значении альфа (прозрачности). Команды glEnable(GL_TEXTURE_GEN_S) и glEnable(GL_TEXTURE_GEN_T) разрешают сферическое наложение.
После всего этого мы перерисовываем сферу, отключаем сферическое наложение и отключаем смешивание.
Конечный результат – это отражение, которое выглядит как блики на пляжном мяче. Так как мы включили сферическое наложение, текстура всегда повернута к зрителю, даже если мяч вращается. Поэтому мы применили смешивание, так что новая текстура не замещает старую (одна из форм мультитекстурирования).
glBindTexture(GL_TEXTURE_2D, texture[2]);// Выбор текстуры 3 (2)
glColor4f(1.0f, 1.0f, 1.0f, 0.4f);// Белый цвет с 40%-й прозрачностью
glEnable(GL_BLEND); // Включить смешивание
// Режим смешивания
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
// Разрешить сферическое наложение
glEnable(GL_TEXTURE_GEN_S);
// Разрешить сферическое наложение
glEnable(GL_TEXTURE_GEN_T);
// Нарисовать новую сферу при помощи новой текстуры
gluSphere(q, 0.35f, 32, 16);
// Текстура будет смешена с созданной для эффекта мультитекстурирования (Отражение)
glDisable(GL_TEXTURE_GEN_S); // Запретить сферическое наложение
glDisable(GL_TEXTURE_GEN_T); // Запретить сферическое наложение
glDisable(GL_BLEND); // Запретить смешивание
}
Следующая функция рисует пол, над которым парит наш мяч. Мы выбираем текстуру пола (EnvWall), и рисуем один текстурированный прямоугольник, расположенный вдоль оси Z.
void DrawFloor() // Рисование пола
{
glBindTexture(GL_TEXTURE_2D, texture[0]);// текстура 1 (0)
glBegin(GL_QUADS); // Начало рисования
glNormal3f(0.0, 1.0, 0.0); // «Верхняя» нормаль
glTexCoord2f(0.0f, 1.0f); // Нижняя левая сторона текстуры
glVertex3f(-2.0, 0.0, 2.0);//Нижний левый угол пола
glTexCoord2f(0.0f, 0.0f); //Верхняя левая сторона текстуры
glVertex3f(-2.0, 0.0,-2.0);//Верхний левый угол пола
glTexCoord2f(1.0f, 0.0f); //Верхняя правая сторона текстуры
glVertex3f( 2.0, 0.0,-2.0);//Верхний правый угол пола
glTexCoord2f(1.0f, 1.0f); //Нижняя правая сторона текстуры
glVertex3f( 2.0, 0.0, 2.0);//Нижний правый угол пола
glEnd(); // конец рисования
}
Теперь одна забавная вещь. Здесь мы скомбинируем все наши объекты и изображения для создания сцены с отражениями.
Начинаем мы с того, что очищаем экран (GL_COLOR_BUFFER_BIT) синим цветом (задан ранее в программе). Также очищаются буфер глубины (GL_DEPTH_BUFFER_BIT) и буфер шаблона (GL_STENCIL_BUFFER_BIT). Убедитесь в том, что вы включили команду очистки буфера шаблона, так как это новая команда для вас и ее легко пропустить. Важно отметить, что при очистке буфера шаблона мы заполняем его нулевыми значениями.
После очистки экрана и буферов мы определяем наше уравнение для плоскости отсечения. Плоскость отсечения нужна для отсечения отражаемого изображения.
Выражение eqr[]={0.0f,-1.0f, 0.0f, 0.0f} будет использовано, когда мы будем рисовать отраженное изображение. Как вы можете видеть, Y-компонента имеет отрицательное значение. Это значит, что мы увидим пиксели рисунка, если они появятся ниже пола, то есть с отрицательным значением по Y-оси. Любой другой графический вывод выше пола не будет отображаться, пока действует это уравнение.
Больше об отсечении будет позже… читайте дальше.
int DrawGLScene(GLvoid)// Рисование сцены
{
// Очистка экрана, буфера глубины и буфера шаблона
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
// Уравнение плоскости отсечения для отсечения отраженных объектов
double eqr[] = {0.0f,-1.0f, 0.0f, 0.0f};
Итак, мы очистили экран и определили плоскости отсечения. Теперь перейдем к изюминке нашего руководства!
Сначала сбросим матрицу модели. После чего все процессы рисования будут начинаться из центра экрана. Затем мы перемещаемся вниз на 0.6f единиц (для наклонной перспективы пола) и в экран на значение zoom. Для лучшего понимания, для чего мы перемещаемся вниз на 6.0f единиц, я приведу вам простой пример. Если вы смотрите на лист бумаги на уровне своих глаз, вы его едва видите – так он скорее похож на тонкую полоску. Если вы переместите лист немного вниз, он перестанет быть похожим на линию. Вы увидите большую площадь бумаги, так как ваши глаза будут обращены вниз на лист вместо прямого взгляда на его ребро.
glLoadIdentity();// Сброс матрицы модели
// Отдаление и подъем камеры над полом (на 0.6 единиц)
glTranslatef(0.0f, -0.6f, zoom);
Далее мы настроим маску цвета – новую вещь в этом руководстве. Маска представлена 4 значениями : красный, зеленый, синий и альфа-значение(прозрачность). По умолчанию все значения устанавливаются в GL_TRUE.
Если бы значение красной компоненты в команде glColorMask({red},{green},{blue},{alpha}) было установлено в GL_TRUE и в то же самое время все другие значения были равны 0 (GL_FALSE), единственный цвет, который бы мы увидели на экране, это красный. Соответственно, если бы ситуация была обратной (красная компонента равна GL_FALSE, а все остальные равны GL_TRUE), то на экране мы бы увидели все цвета за исключением красного.
Нам не нужно что-либо выводить на экран в данный момент, поэтому установим все четыре значения в 0.
glColorMask(0,0,0,0); // Установить маску цвета
Именно сейчас пойдет речь об изюминке урока… Настроим буфер шаблона и проверку шаблона.
Сначала включается проверка шаблона. После того, как была включена проверка шаблона, мы можем изменять буфер шаблона.
Немного сложно объяснить работу команд, приводимых ниже, так что, пожалуйста, потерпите, а если у вас есть лучшее объяснение, пожалуйста, дайте мне знать. Строка glStencilFunc(GL_ALWAYS, 1, 1) сообщает OpenGL тип проверки, производимой для каждого пикселя выводимого на экран объекта (если пиксель не выводиться, то нет и проверки - прим.перев.).
Слово GL_ALWAYS говорит OpenGL, что тест работает все время. Второй параметр – это значение ссылки, которое мы проверяем в третьей строке, и третий параметр – это маска. Маска – это значение, поразрядно умножаемое операцией AND (логическое умножение из булевой алгебры – прим.перев.) на значение ссылки и сохраняемое в буфере шаблона в конце обработки. Значение маски равно 1, и значение ссылки тоже равно 1. Так что, если мы передадим OpenGL эти параметры, и тест пройдет успешно, то в буфере шаблона сохранится единица (так как 1(ссылка)&1(маска)=1).
Небольшое разъяснение: тест шаблона – это попиксельная проверка изображения объектов, выводимых на экран во время работы теста. Значение ссылки, обработанное операцией логического умножения AND на значение маски, сравнивается с текущим значением в буфере шаблона в соответствующем пикселе, также обработанным операцией AND на значение маски.
Третья строка проверяет три различных состояния, основываясь на функции проверки шаблона, которую мы решили использовать. Первые два параметра – GL_KEEP, а третий -GL_REPLACE.
Первый параметр говорит OpenGL что делать в случае если тест не прошел. Так как этот параметр у нас установлен в GL_KEEP, в случае неудачного завершения теста (что не может случится, так как у нас вид функции установлен в GL_ALWAYS), мы оставим состояние буфера в том виде, в котором оно было на это время.
Второй параметр сообщает OpenGL о том, что делать, если тест шаблона прошел успешно, а тест глубины – нет. Далее в программе мы так и так отключаем тест глубины, поэтому этот параметр может быть игнорирован.
И, наиболее важный, третий параметр. Он сообщает OpenGL о том, что надо делать в случае, если весь тест пройден успешно. В приведенном ниже участке программы мы говорим OpenGL, что надо заместить (GL_REPLACE) значение в буфере шаблона. Значение, которое мы помещаем в буфер шаблона, является результатом логического умножения (операция AND) нашего значения ссылки и маски, значение которой равно 1.
Следующее, что нам надо сделать после указания типа теста, это отключить тест глубины и перейти к рисованию пола.
А теперь расскажу обо всем этом попроще.
Мы указываем OpenGL, что не надо ничего не отображать на экране. Значит, во время рисования пола мы ничего не должны видеть на экране. При этом любая точка на экране в том месте, где должен нарисоваться наш объект (пол) будет проверена выбранным нами тестом шаблона. Сначала буфер шаблона пуст (нулевые значения). Нам требуется, чтобы в том, месте, где должен был бы появиться наш объект (пол), значение шаблона было равной единице. При этом нам незачем самим заботиться о проверке. В том месте, где пиксель должен был бы нарисоваться, буфер шаблона помечается единицей. Значение GL_ALWAYS это обеспечивает. Это также гарантируют значения ссылки и маски, установленные в 1. Во время данного процесса рисования на экран ничего не выводится, а функция шаблона проверяет каждый пиксель и устанавливает в нужном месте 1 вместо 0.
// Использовать буфер шаблона для «пометки» пола
glEnable(GL_STENCIL_TEST);
// Всегда проходит, 1 битовая плоскость, маска = 1
glStencilFunc(GL_ALWAYS, 1, 1); // 1, где рисуется хоть какой-нибудь полигон
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glDisable(GL_DEPTH_TEST);// Отключить проверку глубины
DrawFloor();// Рисование пола (только в буфере шаблона)
Теперь у нас есть невидимая шаблонная маска пола. Пока действует проверка шаблона, пиксели будут появляться, только в тех местах, где в буфере шаблона будет установлена 1. И он у нас устанавливается в 1 в том месте, где выводился невидимый пол. Это значит, что мы увидим рисунок лишь в том месте, где невидимый пол установил 1 в буфер шаблона. Этот трюк заставляет появиться отражение лишь на полу и нигде более!
Итак, теперь мы уверены, что отражение мяча нарисуется только на полу. Значит, пора рисовать само отражение! Включаем проверку глубины и отображение всех трех составляющих цвета.
Взамен использования значения GL_ALWAYS в выборе шаблонной функции мы станем использовать значение GL_EQUAL. Значение ссылки и маски оставим равными 1. Для всех операций с шаблоном установим все параметры в GL_KEEP. Проще говоря, теперь любой объект может быть отображен на экране (поскольку цветовая маска установлена в истину для каждого цвета). Во время работы проверки шаблона, выводимые пиксели будут отображаться лишь в том месте, где буфер шаблона установлен в 1 (значение ссылки AND значение маски (1&1) равно 1, что эквивалентно (GL_EQUAL) значению буфера шаблона AND значение маски, что также равно 1). Если в том месте, где рисуется пиксель, буфер шаблона не равен 1, пиксель не отобразится. Значение GL_KEEP запрещает модифицировать буфер шаблона вне зависимости от результата проверки шаблона.
glEnable(GL_DEPTH_TEST); // Включить проверку глубины
glColorMask(1,1,1,1); // Маска цвета = TRUE, TRUE, TRUE, TRUE
glStencilFunc(GL_EQUAL, 1, 1); // Рисуем по шаблону (где шаблон=1)
// (то есть в том месте, где был нарисован пол)
// Не изменять буфер шаблона
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
Теперь подключим плоскость отсечения для отражения. Эта плоскость задается массивом eqr, и разрешает рисовать только те объекты, которые выводятся в пространстве от центра экрана (где находится наш пол) и ниже. Это способ для того, чтобы не дать отражению мяча появиться выше центра пола. Будет некрасиво, если он это сделает. Если вы еще не поняли, что я имею ввиду, уберите первую строку в приведенном ниже коде и переместите клавишами исходный мяч (не отраженный) через пол.
После подключения плоскости отсечения plane0(обычно применяют от 0 до 5 плоскостей отсечения), мы определяем ее, передав параметры из массива eqr.
Сохраняем матрицу (относительно ее позиционируются все объекты на экране) и применяем команду glScalef(1.0f,-1.0f,1.0f) для поворота всех вещей сверху вниз (придавая отражению реальный вид). Негативное значение для Y-параметра в этой команде заставляет OpenGL рисовать все в положении с обратной координатой Y (то есть, «вниз головой» - прим.перев.). Это похоже на переворачивание картинки сверху вниз. Объект с положительным значением по оси Y появляется внизу экрана, а не вверху. Если вы поворачиваете объект к себе, он поворачивается от вас (словом, представьте себя Алисой – прим.перев.). Любая вещь будет перевернута, пока не будет восстановлена матрица или не отработает та же команда с положительным значением Y-параметра (1) (glScalef({x},1.0f,{z}).
glEnable(GL_CLIP_PLANE0);// Включить плоскость отсечения для удаления
// артефактов(когда объект пересекает пол)
glClipPlane(GL_CLIP_PLANE0, eqr);// Уравнение для отраженных объектов
glPushMatrix(); // Сохранить матрицу в стеке
glScalef(1.0f, -1.0f, 1.0f); // Перевернуть ось Y
Первая нижеприведенная строка перемещает наш источник света в позицию, заданную в массиве LightPos. Источник света должен освещать правую нижнюю часть отраженного мяча, имитируя почти реальный источник света. Позиция источника света также перевернута. При рисовании «настоящего» мяча (мяч над полом) свет освещает правую верхнюю часть экрана и создает блик на правой верхней стороне этого мяча. При рисовании отраженного мяча источник света будет расположен в правой нижней стороне экрана.
Затем мы перемещаемся вниз или вверх по оси Y на значение, определенное переменной height. Перемещение также переворачивается, так что если значение height =0.5f, позиция перемещения превратится в -5.0f. Мяч появится под полом вместо того, чтобы появиться над полом!
После перемещения нашего отраженного мяча, нам нужно повернуть его по осям X и Y на значения xrot и yrot соответственно. Запомните, что любые вращения по оси X также переворачиваются. Так, если верхний мяч поворачивается к вам по оси X, то отраженный мяч поворачивается от вас.
После перемещения и вращения мяча нарисуем его функцией DrawObject(), и восстановим матрицу из стека матриц, для восстановления ее состояния на момент до рисования мяча. Восстановленная матрица прекратит отражения по оси Y.
Затем отключаем плоскость отсечения (plan0), так как нам больше не надо ограничивать рисование нижней половиной экрана, и отключаем шаблонную проверку, так что теперь мы можем рисовать не только в тех точках экрана, где должен быть пол.
Заметьте, что мы рисуем отраженный мяч раньше пола.
// Настройка источника света Light0
glLightfv(GL_LIGHT0, GL_POSITION, LightPos);
glTranslatef(0.0f, height, 0.0f);// Перемещение объекта
// Вращение локальной координатной системы по X-оси
glRotatef(xrot, 1.0f, 0.0f, 0.0f);
// Вращение локальной координатной системы по Y-оси
glRotatef(yrot, 0.0f, 1.0f, 0.0f);
DrawObject();// Рисование мяча (для отражения)
glPopMatrix(); // Восстановить матрицу
glDisable(GL_CLIP_PLANE0);// Отключить плоскость отсечения
// Отключение проверки шаблона
glDisable(GL_STENCIL_TEST);
Начнем эту секцию с позиционирования источника света. Так как ось Y больше не перевернута, свет будет освещать верхнюю часть экрана, а не нижнюю.
Включаем смешивание цветов, отключаем освещение и устанавливаем компоненту прозрачности в 80% в команде glColor4f(1.0f,1.0f,1.0f,0.8f). Режим смешивания настраивается командой glBlendFunc(), и полупрозрачный пол рисуется поверх отраженного мяча.
Если бы мы сначала нарисовали пол, а затем – мяч (как нам подсказывает логика – прим.перев.), результат выглядел бы не очень хорошо. Нарисовав мяч, а затем – пол, вы увидите небольшой участок пола, смешанный с рисунком мяча. Когда я посмотрю в синее зеркало, я предположу, что отражение будет немного синим.
Нарисовав сначала пол, последующим отображением пола мы придадим отраженному изображению мяча легкую окраску пола.
glLightfv(GL_LIGHT0, GL_POSITION, LightPos);// Положение источника
// Включить смешивание (иначе не отразится мяч)
glEnable(GL_BLEND);
// В течение использования смешивания отключаем освещение
glDisable(GL_LIGHTING);
// Цвет белый, 80% прозрачности
glColor4f(1.0f, 1.0f, 1.0f, 0.8f);
// Смешивание, основанное на «Source Alpha And 1 Minus Dest Alpha»
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
DrawFloor();// Нарисовать пол
Теперь нарисуем «настоящий» мяч (парящий над полом). При рисовании пола освещение было отключено, но теперь мы опять его включим.
Так как смешивание нам более не потребуется, мы его отключаем. Если мы этого не сделаем, изображение мяча смешается с изображением пола. Нам не нужно, чтобы мяч выглядел, как его отражение, поэтому мы и отключаем смешивание цветов.
Мы не будем отсекать «настоящий» мяч. Если мяч будет проходить через пол, мы должны видеть его выходящим из пола снизу. Если мы будем использовать отсечение, мяч снизу пола не появится. При возникновении необходимости запретить мячу появляться снизу пола вы можете применить значение плоскости отсечения, где будет указано положительное значение Y-координаты. При этом мяч будет виден, только когда он будет рисоваться в верхней части экрана, до той Y-координаты, которую вы укажете в выражении плоскости отсечения. В данном демонстрационном примере у нас нет необходимости этого делать, поэтому мяч будет виден по обе стороны пола.
Затем мы перемещаемся на позицию вывода, заданную в переменной heght. Только теперь ось Y не перевернута, поэтому мяч двигается в направлении, противоположном направлению движения отраженного мяча.
Мяч вращается, и, опять же, поскольку ось Y на данный момент не перевернута, мяч будет вращаться в направлении, обратном направлению вращения отраженного мяча. Если отраженный мяч вращается к вам, «реальный» мяч вращается от вас. Это дополняет иллюзию отражения.
После перемещения и поворота мы рисуем мяч.
glEnable(GL_LIGHTING);// Включить освещение
glDisable(GL_BLEND); // Отключить смешивание
glTranslatef(0.0f, height, 0.0f);// Перемещение мяча
glRotatef(xrot, 1.0f, 0.0f, 0.0f);// Поворот по оси X
glRotatef(yrot, 0.0f, 1.0f, 0.0f);// Поворот по оси Y
DrawObject(); // Рисование объекта
Следующий код служит для поворота мяча по осям X и Y. Для поворота по оси X увеличивается переменная xrot на значение переменной xrotspeed. Для поворота по оси Y увеличивается переменная yrot на значение переменной yrotspeed. Если xrotspeed имеет слишком большое позитивное или негативное значение, мяч будет крутиться быстрее, чем, если бы xrotspeed было близко к нулю. То же касается и yrotspeed. Чем больше yrotspeed, тем быстрее мяч крутится по оси Y.
Перед тем, как вернуть TRUE, выполняется команда glFlush(). Эта команда указывает OpenGL выполнить все команды, переданные ему в конвейер, что помогает предотвратить мерцание на медленных видеокартах.
xrot += xrotspeed; // Обновить угол вращения по X
yrot += yrotspeed; // Обновить угол вращения по Y
glFlush(); // Сброс конвейера OpenGL
return TRUE; // Нормальное завершение
}
Следующий код обрабатывает нажатия клавиш. Первые 4 строки проверяют нажатие вами 4 клавиш (для вращения мяча вправо, влево, вниз, вверх).
Следующие 2 строки проверяют нажатие вами клавиш ‘A’ или ‘Z’. Клавиша ‘A’ предназначена для приближения сцены, клавиша ‘Z’ – для отдаления.
Клавиши ‘PAGE UP’ и ’ PAGE UP’ предназначены для вертикального перемещения мяча.
void ProcessKeyboard() // Обработка клавиатуры
{
if (keys[VK_RIGHT]) yrotspeed += 0.08f;// Вправо
if (keys[VK_LEFT]) yrotspeed -= 0.08f; // Влево
if (keys[VK_DOWN]) xrotspeed += 0.08f; // Вверх
if (keys[VK_UP]) xrotspeed -= 0.08f; // Вниз
if (keys['A']) zoom +=0.05f; // Приближение
if (keys['Z']) zoom -=0.05f; // Отдаление
if (keys[VK_PRIOR]) height +=0.03f; // Подъем
if (keys[VK_NEXT]) height -=0.03f; // Спуск
}
Функция KillGLWindow() не меняется, поэтому пропущена.
GLvoid KillGLWindow(GLvoid)// Удаление окна
Также можно оставить и следующую функцию - CreateGLWindow(). Для большей уверенности я включил ее полностью, даже если поменялась всего одна строка в этой структуре:
static PIXELFORMATDESCRIPTOR pfd=
// pfd говорит Windows о наших запросах для формата пикселя
{
sizeof(PIXELFORMATDESCRIPTOR), // Размер структуры
1, // Номер версии
PFD_DRAW_TO_WINDOW | // Формат должен поддерживать Window
PFD_SUPPORT_OPENGL | // Формат должен поддерживать OpenGL
PFD_DOUBLEBUFFER, // Нужна двойная буферизация
PFD_TYPE_RGBA, // Формат данных- RGBA
bits, // Глубина цвета
0, 0, 0, 0, 0, 0, // Игнорируются биты цвета
0, // Нет альфа-буфера
0, // Игнорируется смещение бит
0, // Нет аккумулирующего буфера
0, 0, 0, 0, // Игнорируются биты аккумуляции
16, // 16-битный Z-буфер (глубины)
Только одно изменение в этой функции – в приведенной ниже строке. ОЧЕНЬ ВАЖНО: вы меняете значение с 0 на 1 или любое другое ненулевое значение. Во всех предыдущих уроках значение в строке ниже было равным 0. Для использования буфера шаблона это значение должно быть больше либо равным 1. Оно обозначает количество битовых планов буфера шаблона.
1, // Использовать буфер шаблона (* ВАЖНО *)
0, // Нет вспомогательного буфера
PFD_MAIN_PLANE, // Основной уровень рисования
0, // Не используются
0, 0, 0 , // Нет маски уровня
};
WndProc() не изменилась, поэтому здесь не приводится.
Здесь тоже ничего нового. Типичный запуск WinMain().
Меняется только заголовок окна, в котором содержится информация о названии урока и его авторах. Обратите внимание, что вместо обычных параметров экрана 640, 480, 16 в команду создания окна передаются переменные resx, resy и resbpp соответственно.
// Создание окна в Windows
if (!CreateGLWindow("Banu Octavian & NeHe's Stencil & Reflection Tutorial",
resx, resy, resbpp, fullscreen))
{
return 0;// Выход, если не создалось
}
while(!done)// Цикл, пока done=FALSE
{
// Выборка сообщений
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
if (msg.message==WM_QUIT) // Выход?
{
done=TRUE;// Если да
}
else // Если нет, обработка сообщения
{
TranslateMessage(&msg); // Транслировать сообщение
DispatchMessage(&msg);
}
}
else // Если нет сообщений
{ // Отрисовка сцены. Ожидание клавиши ESC или
// сообщения о выходе от DrawGLScene()
if (active) // Программа активна?
{
if (keys[VK_ESCAPE])// ESC нажата?
{
done=TRUE;// Если да, выход
}
else// Иначе - рисование
{
DrawGLScene();// Рисование сцены
SwapBuffers(hDC);//Переключить буфера
Вместо проверки нажатия клавиш в WinMain(), мы переходим к нашей функции обработки клавиатуры ProcessKeyboard(). Запомните, что эта функция вызывается, только если программа активна!
ProcessKeyboard();// Обработка нажатий клавиш
}
}
}
} // Конец работы
KillGLWindow(); // Удалить окно
return (msg.wParam);// Выход из программы
}
Я надеюсь, что вам понравилось это руководство. Понимаю, что оно задаст вам немного работы. Это было одно из самых трудных руководств, написанных мною. Оно несложно для моего понимания того, что оно делает, и какие команды используются для создания эффектов, но когда вы попытаетесь объяснить это, понимая, что большинство программистов даже не слышали о буфере шаблона, это трудно.
Если вам что-то показалось лишним в программе или если вы обнаружили какие-либо ошибки в руководстве или программе, пожалуйста, дайте мне знать. Как обычно, я пытался сделать данное руководство наилучшим, насколько смог, и ваши отзывы будут наиболее ценны.