OpenGL: Маскинг и режим ortho

Возможно, вы когда-либо хотели отобразить только часть текстуры, не изменяя полигона, на который она наложена — например, деревья, кусты, частицы, или, например, интерфейс. Данный урок научит вас этой важной технологии.

Вы могли раньше экспериментировать с блендингом. Например: glBlendFunc (GL_SRC_ALPHA), с включенным блендингом (glEnable (GL_BLEND_FUNC)) — смешает текстуру с бекграундом. Чем ближе цвет на текстуре к черному, тем меньше прозрачности. Если цвет на текстуре белый, это место будет прозрачным, и будет отображено всё, что находится за ним.
Например, если у вас есть картинка дерева с черным фоном, будет отображено только дерево, без черного бекграунда. Проблема в том, что так как само дерево не белое, оно тоже будет выглядеть полупрозрачным. Это нормально с частицами, но если вы хотите, чтобы картинка была чистой и красивой, вам нужно либо изображение с альфа-каналом, либо наложение маски (masking). Маскинг делается так, что отображается только та часть изображения, которая закрашена черным на маске. То есть вы должны иметь 2 картинки: одну — нормальную, вторую — черно-белую, содержащую маску. Детали будут разъяснены внизу.

Для демонстрации маскинга мы нарисуем круг прицела. Вокруг мы нарисуем комнату — она будет видна сквозь прозрачную часть прицела. Если вы посмотрите битмап маски прицела, то увидите, что его центр — белый, что значит, что он НЕ будет отрисован.

Контроль:
Вверх-Вниз, w/s — zoom, приближение и отдаление прицела.
Влево-Вправо, a/d — вращение по кругу.
Пробел — вкл/выкл NightVision
ESC — выход.

Кроме изучения маскинга будет затронут режим ortho. Мы уже использовали его, начиная с урока «Подсчет FPS», но теперь я обьясню его подробно.

Исходные коды взяты из урока «загрузка текстур».
Файлы init.cpp, main.h, texture.cpp, texture.h остаются без изменений.

Файл main.cpp:

// В начале увеличим количество текстур до 4:
TextureImage textures[4];// Создадим переменную для хранения угла вращения:
float rotateY = 0;
// Переменная, переключающая нас в режим ночноги видения и обратно:
bool bNightVision = false;
// Значение ZOOM:
float Zoom=0;

// Изменим функцию Init (): загрузим четыре текстуры и сделаем пространство
// синим, а не черным, как обычно:
void Init(HWND hWnd)
{
g_hWnd = hWnd;
GetClientRect(g_hWnd, &g_rRect);
InitializeOpenGL(g_rRect.right, g_rRect.bottom);

// Инициализируем текстурный класс и загружаем текстуры.
Texture = new CTexture();

Texture->LoadTexture(IL_BMP, «Wall.bmp», &textures[0]);
Texture->LoadTexture(IL_BMP, «floor.bmp», &textures[1]);
Texture->LoadTexture(IL_BMP, «CrossHairMask.bmp», &textures[2]);
Texture->LoadTexture(IL_BMP, «CrossHair.bmp», &textures[3]);

// Вместо стандартного черного, делаем пространство синим
glClearColor(.4f, 0, 1, 1);
}

// Добавим функцию обработки ввода:
void KeyPressed()
{
// Как и в уроках камеры, мы используем отдельную функцию для контроля
// движения, т.к. WinProc () значительно медленнее.

// Клавиши зума
if(GetKeyState(VK_UP) & 0×80 || GetKeyState('W') & 0×80) {

// Проверим, не превысили ли мы порог (13), если нет — увеличиваем зум
if(g_zoom < 13) g_zoom += 0.5f;
}
if(GetKeyState(VK_DOWN) & 0×80 || GetKeyState('S') & 0×80) {

// Убедимся, что мы не зумимся назад, если нет — уменьшаем зум
if(g_zoom > 0)  g_zoom -= 0.5f;
}

// Клавиши вращения
if(GetKeyState(VK_LEFT) & 0×80 || GetKeyState('A') & 0×80) {
// Уменьшим угол вращения мира
g_rotateY -= 2;
}
if(GetKeyState(VK_RIGHT) & 0×80 || GetKeyState('D') & 0×80) {
// Увеличим угол вращения мира
g_rotateY += 2;
}
}

//////////////
// В функции MainLoop () сразу после RenderScene (); добавьте вызов:
KeyPressed();
//////////////

// Тут начинаются вещи, с которыми мы обычно не имели дела — матрица проекции.
// С вызовами функций OrthoMode () и PerspectiveMode () происходит замена между
// 3д точками и 2д точками. Это значит, что вместо передавания OpenGL трехмерных
// координат, в ortho-режиме мы передаем ему пикселные координаты экрана.
// Это очень полезный режим, поскольку оберегает нас от огромных рассчетов,
// когда нам нужно рисовать интерфейс поверх 3д сцены. Когда вы вращаете или
// хоть как-то изменяете положение камеры, вслед за ней нужно изменять положение
// интерфейса, что очень раздражает.
// В самом начале вам нужно установить требуемую глубину, то есть нужно найти
// значение Z, с которым 2д картинка лучше всего смотрится. Если значение будет
// неподходящим, картинка может, например, стать пиксельной, что не есть гут.
// Кроме того, это работает намного быстрее glDrawPixels, и вам не нужно трогать
// растеризацию, плюс в можете использовать прозрачность и другие эффекты OpenGL.

///////////////////////////////// ORTHO MODE \\\\\\\\\\\\\\\\*
/////
/////           Новая функция: переключение в 2D режим
/////
///////////////////////////////// ORTHO MODE \\\\\\\\\\\\\\\\*

void OrthoMode(int left, int top, int right, int bottom)
{
// Эта функция принимает координаты квадрата для нашего 2д экрана.
// Ниже нам нужно включить матрицу проекции. Чтобы это сделать,
// вызываем glMatrixMode (GL_PROJECTION), чтобы указать, какой нам нужен режим.
// Далее мы загружаем новую матрицу, чтобы всё инициализировалось до того, как
// мы перешли в режим ortho. Перейдя в 2д режим вызовом glOrtho (), мы изменяем
// матрицу на GL_MODELVIEW, которая подготавливает нас к рендеру мира, используя
// матрицу моделей. Также мы инициализируем новую матрицу моделей для 2д рендера,
// прежде чем что-то рисовать.

// Переключаемся на матрицу проекции, чтобы перейти в режем ortho, не perspective
glMatrixMode(GL_PROJECTION);

// Входим в новую матрицу, чтобы потом просто выйти из неё,
// для возвращения в режим перспективы
glPushMatrix();

// Сбросим нашу текущую матрицу
glLoadIdentity();

// Передадим OpenGL экранные координаты. Лево, право, низ, верх.
// Последние 2 параметра — ближняя и дальняя плоскости.
glOrtho( left, right, bottom, top, 0, 1 );

// Переключимся в modelview, чтобы рендерить общую картинку
glMatrixMode(GL_MODELVIEW);

// Обнулим текущую матрицу
glLoadIdentity();
}

///////////////////////////////// PERSPECTIVE MODE \\\\\\\\\\\\\\\\*
/////
/////       Новая функция: возвращает нас из 2д режима в 3д.
/////
///////////////////////////////// PERSPECTIVE MODE \\\\\\\\\\\\\\\\*

void PerspectiveMode()
{
// На самом деле, эта ф-я не переключается в режим перспективы, мы не
// используем gluPerspective (), но если вы вспомните OrthoMode (), мы
// работаем в новой матрице. Всё, что нам надо — выйти из этой матрицы,
// и вернутся и изначальную матрицу перспективы. Итак, перед выходом из
// матрицы, нам нужно сказать OpenGL, какую матрицу мы хотим использовать
// — изменением текущей матрицы на перспективу. Потом мы можем вернутся
// в матрицу modelview и рендерить всё как обычно.

// Входим в режим матрицы проекции
glMatrixMode( GL_PROJECTION );

// Выходим из последней матрицы, возвращаясь в матрицу с режимом перспективы.
glPopMatrix();

// Возвращаемся в матрицу моделей
glMatrixMode( GL_MODELVIEW );

// Теперь мы должны быть в нормальном 3д режиме с перспективой
}

///////////////////////////////// DRAW ROOM \\\\\\\\\\\\\\\\*
/////
/////           Новая функция: рисует стены и пол
/////
///////////////////////////////// DRAW ROOM \\\\\\\\\\\\\\\\*

void DrawRoom()
{
// Чтобы лучше продемонстрировать прозрачность прицела, я создал стены и пол,
// на которые можно смотреть. Мы наложим текстуры пола и стены на созданные
// квадраты. Наша камера находится в центре мира, а стены
// — на 15 единиц по бокам. Так как квадраты большие, нам нужно повторять
// текстуры несколько раз вместо растягивания на всю плоскость.
// Вот почему координаты U и V более единицы.

// Биндим текстуру на стены
glBindTexture(GL_TEXTURE_2D,  textures[0].texID);

// 4 длинных квадрата — стены
glBegin(GL_QUADS);

////////////////////// Задняя стена ///////////////////////
glTexCoord2f(0.0f, 2.0f);   glVertex3f(15, 1, 15);
glTexCoord2f(0.0f, 0.0f);   glVertex3f(15, 1, 15);
glTexCoord2f(12.0f, 0.0f);  glVertex3f(15, 1, 15);
glTexCoord2f(12.0f, 2.0f);  glVertex3f(15, 1, 15);
////////////////////// Передняя стена ///////////////////////
glTexCoord2f(0.0f, 2.0f);   glVertex3f(15, 1, 15);
glTexCoord2f(0.0f, 0.0f);   glVertex3f(15, 1, 15);
glTexCoord2f(12.0f, 0.0f);  glVertex3f(15, 1, 15);
glTexCoord2f(12.0f, 2.0f);  glVertex3f(15, 1, 15);
////////////////////// Левая стена ///////////////////////
glTexCoord2f(0.0f, 2.0f);   glVertex3f(15, 1, 15);
glTexCoord2f(0.0f, 0.0f);   glVertex3f(15, 1, 15);
glTexCoord2f(12.0f, 0.0f);  glVertex3f(15, 1, 15);
glTexCoord2f(12.0f, 2.0f);  glVertex3f(15, 1, 15);
////////////////////// Правая стена ///////////////////////
glTexCoord2f(0.0f, 2.0f);   glVertex3f(15, 1, 15);
glTexCoord2f(0.0f, 0.0f);   glVertex3f(15, 1, 15);
glTexCoord2f(12.0f, 0.0f);  glVertex3f(15, 1, 15);
glTexCoord2f(12.0f, 2.0f);  glVertex3f(15, 1, 15);
// Стены нарисовали.
glEnd();

// Теперь нам нужно нарисовать пол. Мы повторяем текстуру по полу
// 16 раз в каждую сторону. Передаём 16.0f в текстурных координатах.

// Биндим текстуру пола
glBindTexture(GL_TEXTURE_2D,  textures[1].texID);

// Квадрат — пол
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 16.0f);  glVertex3f(15, 1, 15);
glTexCoord2f(0.0f, 0.0f);   glVertex3f(15, 1, 15);
glTexCoord2f(16.0f, 0.0f);  glVertex3f(15, 1, 15);
glTexCoord2f(16.0f, 16.0f); glVertex3f(15, 1, 15);
// Нарисовали пол
glEnd();
}

//////////////////////
// Ещё одна вещь: добавим в WinProc обработку пробела для переключения
// режима нормального/ночного видения. Теперь блок WM_KEYDOWN выглядит так:

case WM_KEYDOWN:
switch(wParam) {    // Если нажата клавиша
case VK_ESCAPE: // Если нажат ескейп
PostQuitMessage(0); // Выходим
break;

case VK_SPACE:  // Если нажат пробел
bNightVision = !bNightVision; // Вкл/выкл ночное видение
break;

}
break;
////////////////////////////

////////////////////////////////////////////////////////
//
// И, наконец, главный участок нашей программы:

void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();

// Вместо использования класса камеры я написал небольшой кусок кода попроще,
// чтобы продемонстрировать пример зума вперед-назад. В идеале у нас будет
// позиция камеры и несколько обьектов «камера» на одной линии. Тогда при зуме
// можно будет просто переключаться между ними.

// Позиция       Направление     Верт. вектор
gluLookAt(0, 0, Zoom,     0, 0, Zoom + 1,     0, 1, 0);

// Вращаем мир вокруг оси Z
glRotatef(rotateY , 0, 1, 0);

// Текстурированные стены и пол
DrawRoom();

// Прежде, чем рисовать текстуру прицела, нужно перейти в режим ortho. Мы передаем
// ширину и высоту экрана, чтобы использовать его правые и нижние координаты
OrthoMode(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

// Если ночное видение включено, окрашиваем мир в зеленый цвет, иначе — в нормальный.
if(bNightVision)
glColor4f(0, 1, 0, 1);
else
glColor4f(1, 1, 1, 1);

///////////////////// MASKING ///////////////////////////////

// Когда комната нарисована, начинаем наш маскинг. Чтобы всё работало,
// НЕОБХОДИМО ОТКЛЮЧИТЬ ТЕСТ ГЛУБИНЫ. Иначе работать не будет.
// Прежде, чем рендерить нормальную картинку, рендерим её маску.
// Метод блендинка будет следующим: (GL_DST_COLOR, GL_ZERO).

// Тест глубины ДОЛЖЕН быть отключен
glDisable(GL_DEPTH_TEST);

// Включаем блендинг
glBlendFunc(GL_DST_COLOR,GL_ZERO);
glEnable(GL_BLEND);

// Биндим текстуру маски к нашему 2д квадрату.
glBindTexture(GL_TEXTURE_2D,  textures[2].texID);

// Отображаем 2д квадрат с маской
glBegin(GL_QUADS);
// Обратите внимание, что находясь в 2д режиме, мы используем glVertex2f (),
// передавая не вершины, а экранные координаты. Это делает отрисовку интерфейса
// значительно легче. Похоже всё это на обычную 2д графику.
// Текстурные координаты остаются без изменений.

// Отображаем верхнюю левую точку 2д рисунка
glTexCoord2f(0.0f, 1.0f);   glVertex2f(0, 0);
// Нижнюю левую
glTexCoord2f(0.0f, 0.0f);   glVertex2f(0, SCREEN_HEIGHT);
// Нижнюю правую
glTexCoord2f(1.0f, 0.0f);   glVertex2f(SCREEN_WIDTH, SCREEN_HEIGHT);
// Верхнюю правую
glTexCoord2f(1.0f, 1.0f);   glVertex2f(SCREEN_WIDTH, 0);
// Нарисовали
glEnd();

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

// Включаем режим блендинга «one to one»
glBlendFunc(GL_ONE,GL_ONE);

// Биндим текстуру прицела к следующему 2д квадрату
glBindTexture(GL_TEXTURE_2D,  textures[3].texID);

// 2д квадрат с текстурой прицела
glBegin(GL_QUADS);
// верх лево
glTexCoord2f(0.0f, 1.0f);   glVertex2f(0, 0);
// низ лево
glTexCoord2f(0.0f, 0.0f);   glVertex2f(0, SCREEN_HEIGHT);
// низ право
glTexCoord2f(1.0f, 0.0f);   glVertex2f(SCREEN_WIDTH, SCREEN_HEIGHT);
// верх право
glTexCoord2f(1.0f, 1.0f);   glVertex2f(SCREEN_WIDTH, 0);
// нарисовали
glEnd();

// закончив, мы можем отключить блендинг и включить тест глубины
glDisable(GL_BLEND);
glEnable(GL_DEPTH_TEST);

// Вернемся назад в режим перспективы.
PerspectiveMode();

SwapBuffers(g_hDC);
}

 

Полезный эффект, верно? Вы можете заметить, что края прицела несколько размазаны.
Это потому, что мы берём маленькую текстуру и растягиваем её на весь экран. Я сделал маленькую текстуру, так как некоторые старые видеокарты не поддерживают текстуры, бОльшие, чем 512×512.

Давайте рассмотрим процесс ещё раз.

Маскирование.
Техника маскрирования позволяет вам, например, вывести на прямоугольник рисунок дерева с прозрачным фоном вместо сплошного. Это достигается созданием маски, типа 2Д, накладывающейся перед самим изображением, а после этого накладывается основная текстура, прозрачная в местах, обозначенных наложенной перед ней маской. Обратная сторона медали — то, что вам нужно дважды рендерить изображение. Я предлагаю вместо этого использовать текстуры с альфа-каналом.

Режим Ortho.
Режим Ortho — режим проекции, позволяющий рисовать примитивы в 2Д. Вместо glVertex3f () используется glVertex2f (), в которую передаются экранные 2д координаты вместо мировых 3д. Это очень полезно, если имеешь дело с интерфейсом или чем-то, что должно оставатся на экране независимо от движения камеры.

Итак, как так получается, что положение картинки на экране остается неизменным независимо от движения камеры? На самом деле всё просто. Вспомните, в OrthoMode () мы вызвали новую матрицу прямо перед вызовом glLoadIdentity (). Мы сделали так, что текущая матрица, с которой мы работаем, НЕ задета сбросом новой матрицы. Но после сброса матрицы уже НЕТ вращения или перемещения, применённых к предидущей матрице, вплоть до выхода из матрицы вызовом glPopMatrix (). Проще говоря, мы как бы говорим «Таймаут, мне нужно стартовать свежую матрицу, но не удаляйте предидущую, так как я вернусь в неё через секунду».

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

Надеюсь, урок кому-нибуть помог!

Исходные коды к уроку

Понравилась статья? Поделиться с друзьями: