OpenGL: Motion Blur

Код этого урока взят из урока «загрузка текстур», кроме того, добавлен соответствующий код из уроков об Ortho mode и о рендере в текстуру. Так что перед работой с этим уроком советую вам просмотреть предыдущие, если вы их не проходили =)

В этом уроке показана технология создания эффекта MotionBlur — размытого движения — довольно-таки быстрым способом. Может быть, вы слышали о другом способе — создании буффера аккумуляции, но очень уж это медленно для риал-тайм рендера. Более практичный способ — рендерить текущий кадр в текстуру, а потом в режиме ortho рендерить текстуру на экран.
Изменить число рендеров на кадр можно будет клавишами ВЛЕВО и ВПРАВО. Чем больше значение, тем более реалистично выглядит кадр.
Клавиши 1, 2 и 3 изменяют размер создаваемой текстуры.
Пробел останавливает и стартует вращение.

Итак, переходим к коду.
Файл main.cpp:

// Увеличим количество текстур до двух:
TextureImage textures[2];// Добавим несколько переменных вверху файла.// Первая будет хранить число рендеров/кадр:
int g_BlurRate = 50;
// Вторая — вкл/выкл вращения примитива:
bool g_bRotate = true;
// Эти три — значения вращения по разным осям:
float g_RotateX = 0, g_RotateY = 0, g_RotateZ = 0;
// Размер (разрешение) экрана, который будем рендерить в текстуру:
int g_Viewport = 512;
// А это — прототип функции, создаюшей пустую текстуру:
void CreateRenderTexture(TextureImage textureArray[],int size, int channels, int type, int textureID);

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

Texture = new CTexture();

// Инициализируем пустую текстуру для рендера. Передаём ID текстуры,
// размер, каналы (3 для РГБ), тип (РГБ) и ID текстуры.
CreateRenderTexture(textures, 512, 3, GL_RGB, 0);
// Загрузим текстуру кирпичей в текстуру с ID 1
Texture->LoadTexture(IL_BMP, «Brick.bmp», &textures[1]);
}

// Добавим функции для перехода в режим Ortho и обратно. Подробные обьяснения — в прошлых
// уроках.
void OrthoMode(int left, int top, int right, int bottom)
{
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();

glOrtho( left, right, bottom, top, 0, 1 );

glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}

void PerspectiveMode()
{
glMatrixMode( GL_PROJECTION );
glPopMatrix();
glMatrixMode( GL_MODELVIEW );
}

// Добавим функцию, создающую нашу вращающуюся фигуру:
void CreateBox(int textureID, float x, float y, float z, float width,
float height,  float length, float uscale, float vscale)
{
// Если ID текстуры >= 0, наложим текстуру:
if(textureID >= 0) glBindTexture(GL_TEXTURE_2D, textures[textureID].texID);

// Расположим центр в координатах x,y,z:
x = x width  / 2;
y = y height / 2;
z = z length / 2;

// Начинаем рисовать фигуры в режиме GL_QUADS:
glBegin(GL_QUADS);

// Задняя сторона
glTexCoord2f(0.0f,     0.0f); glVertex3f(x,y,   z);
glTexCoord2f(0.0f,   vscale); glVertex3f(x,y + height, z);
glTexCoord2f(uscale, vscale); glVertex3f(x + width, y + height, z);
glTexCoord2f(uscale,   0.0f); glVertex3f(x + width, y,  z);

// Передняя сторона
glTexCoord2f(uscale,   0.0f); glVertex3f(x,y,z + length);
glTexCoord2f(uscale, vscale); glVertex3f(x,y + height, z + length);
glTexCoord2f(0.0f,   vscale); glVertex3f(x + width, y + height, z + length);
glTexCoord2f(0.0f,     0.0f); glVertex3f(x + width, y,  z + length);

// Нижняя сторона
glTexCoord2f(uscale,   0.0f); glVertex3f(x,y,   z);
glTexCoord2f(uscale, vscale); glVertex3f(x,y,   z + length);
glTexCoord2f(0.0f,   vscale); glVertex3f(x + width, y,  z + length);
glTexCoord2f(0.0f,     0.0f); glVertex3f(x + width, y,  z);

// Верхняя сторона
glTexCoord2f(uscale, vscale); glVertex3f(x,y + height,  z);
glTexCoord2f(uscale,   0.0f); glVertex3f(x,y + height,  z + length);
glTexCoord2f(0.0f,     0.0f); glVertex3f(x + width, y + height, z + length);
glTexCoord2f(0.0f,   vscale); glVertex3f(x + width, y + height, z);

// Левая сторона
glTexCoord2f(uscale,   0.0f); glVertex3f(x,y,z);
glTexCoord2f(0.0f,     0.0f); glVertex3f(x,y,   z + length);
glTexCoord2f(0.0f,   vscale); glVertex3f(x,y + height,  z + length);
glTexCoord2f(uscale, vscale); glVertex3f(x,y + height,  z);

// Правая сторона
glTexCoord2f(0.0f,     0.0f); glVertex3f(x + width, y,  z);
glTexCoord2f(uscale,   0.0f); glVertex3f(x + width, y,  z + length);
glTexCoord2f(uscale, vscale); glVertex3f(x + width, y + height, z + length);
glTexCoord2f(0.0f,   vscale); glVertex3f(x + width, y + height, z);

glEnd();
}

// Опишем реализацию CreateRenderTexture ():
void CreateRenderTexture(TextureImage textureArray[],int size, int channels, int type, int textureID)
{
// Создадим указатель на пустые данные текстуры
unsigned int *pTexture = NULL;

// Выделим и инициализируем память под данные текстуры, и направим
// на них указатель pTexture
pTexture = new unsigned int [size * size * channels];
memset(pTexture, 0, size * size * channels * sizeof(unsigned int));

// Передадим текстуру OpenGL и забиндим на ID текстуры:
glGenTextures(1, &textureArray[textureID].texID);
glBindTexture(GL_TEXTURE_2D, textureArray[textureID].texID);

// Create the texture and store it on the video card
// Создадим текстуру и сохраним её на видеокарте
glTexImage2D(GL_TEXTURE_2D, 0, channels, size, size, 0, type, GL_UNSIGNED_INT, pTexture);

// Установим кач-во текстуры
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

// Сохранив текстуру на видеокарте, удалим данные:
delete [] pTexture;
}

// Напишем новую функцию, рассчитывающую кадры:
bool AnimateNextFrame(int desiredFrameRate)
{
static float lastTime = 0.0f;
float elapsedTime = 0.0;

// Текущее время в секундах
float currentTime = GetTickCount() * 0.001f;

// Получим прошедшее с предидущего кадра время
elapsedTime = currentTime lastTime;

// Проверим, больше ли прошедшее время, чем (1 second / framesPerSecond)
if( elapsedTime > (1.0f / desiredFrameRate) )
{
// Сбросим последнее время
lastTime = currentTime;

// Вернем TRUE, чтобы анимировать следующий кадр
return true;
}

// Иначе сейчас анимация не нужна
return false;
}

// Еще одна новая функция, рисующая motion blur:
void RenderMotionBlur(int textureID)
{
// Эта функция создана для рисования текстуры с прозрачным фоном прямо перед
// экраном, чтобы создать рекурсивный эффект смазывания. Мы начинаем, выключая
// тест глубины, устанавливая режим прозрачности, а потом биндим отрендеренную
// текстуру на экран. Далее устанавливаем уровень прозрачности в 90%.
// Перед рендером квадрата, переходим в режим ortho. Отрендерив, возвращаемся
// в режим перспективы, включаем тест глубины и выключаем текстурирование.

// Входим в новую матрицу
glPushMatrix();

// Выключаем тест глубины
glDisable(GL_DEPTH_TEST);

// Включаем прозрачность и задаем её режим
glBlendFunc(GL_SRC_ALPHA,GL_ONE);
glEnable(GL_BLEND);

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

// Установим альфа-прозрачность в 90%
glColor4f(1, 1, 1, 0.9f);

// Переходим в режим ortho
OrthoMode(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);

// Рисуем квадрат с отрендеренной текстурой
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();

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

// Включим тест глубины и выключим прозрачность.
glEnable(GL_DEPTH_TEST);
glDisable(GL_BLEND);

// Вернемся в оригинальную матрицу.
glPopMatrix();
}

// Наконец, переходим к RenderScene ():
void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();

gluLookAt(0, 0, 10,     0, 0, 0,     0, 1, 0);

// Нам нужно вращение куба вокруг всех осей, чтобы лучше увидеть смазывание в разных
// направлениях. Вы можете нажать пробел, чтобы остановить вращение.

glRotatef(g_RotateX, 1.0f, 0.0f, 0.0f); // Вокруг оси X
glRotatef(g_RotateY, 0.0f, 1.0f, 0.0f); // Вокруг оси Y
glRotatef(g_RotateZ, 0.0f, 0.0f, 1.0f); // Вокруг оси Z

// Ниже рендерим в текстуру. Мы можем контролировать количество рендеров в секунду
// нажатием влево-вправо. Чем больше рендеров в секунду, тем лучше качество.

// Рендер в текстуру контролируется ф-ей AnimateNextFrame ()
if( AnimateNextFrame(g_BlurRate) )
{
// Установим вьюпорт в заданное разрешение. Изменяется клавишами 1,2,3
glViewport(0, 0, g_Viewport, g_Viewport);

// Тут мы перерисовываем текущую отрендеренную текстуру перед отрисовкой бокса.
// Передаём ID текстуры.
RenderMotionBlur(0);

// Теперь нужно отрендерить в текстуру вращающийся куб. Нам нужно просто создать
// куб, и он будет уже с заданным вращением.
// параметры — ID текстуры, центр (x,y,z), смещение и UV координаты.
CreateBox(1,    0, 0, 0,    1, 6, 1,    1, 3);

// Прежде, чем копировать экран в текстуру, нужно указать текущую текстуру
// вызовом glBindTexture
glBindTexture(GL_TEXTURE_2D,textures[0].texID);

// Рендерим экран в текстуру
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 0, 0, g_Viewport, g_Viewport, 0);

// Очищаем экран и буфер глубины
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// Возвращаем вьюпорт в нормальное состояние
glViewport(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
}

// Теперь, обновив отрендеренную текстуру, вывелем её на экран.
RenderMotionBlur(0);

// И нарисуем вращающийся куб
CreateBox(1,    0, 0, 0,    1, 6, 1,    1, 3);

// Если куб вращается, увеличим значения вращения
if(g_bRotate)
{
g_RotateX += 2; g_RotateY += 3; g_RotateZ += 2;
}

SwapBuffers(g_hDC);
}

///////////////////////////////////////////////////////
//
// В начале WinProc добавим:
bool bUpdate = false;

// Изменим обработку клавиш.
// Блок WM_KEYDOWN в ф-ии WinProc должен выглядеть так:
case WM_KEYDOWN:
switch(wParam) {
case VK_ESCAPE:
PostQuitMessage(0);
break;

// Мы можем контролировать частоту смазывания клавишами ВЛЕВО-ВПРАВО.
// Также мы можем останавливать вращение пробелом.

case VK_LEFT:  // Если нажата ВЛЕВО

g_BlurRate -= 10; // Уменьшим рендер в секунду
// Убедимся, что не менее 10
if(g_BlurRate < 10) g_BlurRate = 10;

bUpdate = true; // Обновим информацию в заголовке
break;

case VK_RIGHT:  // если нажата ВПРАВОЛ

g_BlurRate += 10;  // Увеличим рендер в секунду

// Убедимся, что не превысили сотню:
if(g_BlurRate > 100) g_BlurRate = 100;
bUpdate = true; // Обновим информацию в заголовке
break;

case VK_SPACE:  g_bRotate = !g_bRotate; // вкл/выкл вращение
break;

case '1':   g_Viewport = 128;// Изменим разрешение до 128
bUpdate = true;     // Обновим информацию в заголовке
break;

case '2':   g_Viewport = 256;       // Изменим разрешение до 256
bUpdate = true; // Обновим информацию в заголовке
break;

case '3':   g_Viewport = 512;   // Изменим разрешение до 512
bUpdate = true; // Обновим информацию в заголовке
break;

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

// И в конец функции WinProc, перед строкой «return lRet», добавим:
if(bUpdate)
{
static char strTitle[255] = {0};
sprintf(strTitle,
«Current Texture Renders Per Second: %d   Current Texture Size: %d»,
g_BlurRate, g_Viewport);

SetWindowText(g_hWnd, strTitle);// Обновим информацию в заголовке
}

 

Смазывание движения классно выглядит, да? Это так, но будьте осторожны — не используйте его постоянно. Хоть этот метод и быстр, но всё, что вы хотите сделать смазанным, рисуется дважды.
А вообще, чем лучше видеокарта, тем более детализированными могут быть текстуры смазывания.
Если вы попробуете установить размер 128×128, увидете, что качество становится так себе.
Я рекомендую использовать как минимум 256×256. Хотя 128 тоже подходит, если движение происходит быстро и далеко от камеры.

Надеюсь, урок вам пригодился.

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

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