четверг, 8 марта 2012 г.

Основы FBO в OpenGL. Часть вторая, простой пример.

Часть первая, немного теории
Часть вторая, простой пример
Часть третья, буфер глубины
Часть четвертая, MRT
Часть пятая, чтение
Часть шестая, R2VB
Ссылки по теме    
Все необходимое о FBO мы уже знаем, теперь закрепим эти знания на практике. Начнем рассмотрения с простейшей задачи - отрендерим всю сцену  в текстуру и выведем на экран скринквад с этой текстурой.


Так как нам для работы потребуется множество вспомогательных функций, не относящихся к теме статьи, то я вынес их реализацию в отдельный модуль – DemoTools. Вкратце о функциях этого модуля:
1
2
3
4
5
6
7
8
9
procedure DrawCube;
procedure DrawScreenQuad ( tex: GLUInt ); overload;
procedure DrawScreenQuad ( Rows, Columns: integer; const tex: array of GLUInt ); overload;
function CreateTextureRGBA32F(width, height: integer): GLUInt;
function CreateTextureRGBA16F(width, height: integer; GenMipmap: boolean=true): GLUInt;
function CreateTextureRGBA8(width, height: integer; GenMipmap: boolean=true): GLUInt;
function CreateDepthTexture(width, height: integer): GLUInt;
function CreateShader(Vert, Frag: string): TShader;
procedure DeleteShader(Shader: TShader);



Функция DrawCube рисует обычный куб, функции DrawScreenQuad – рисуют прямоугольник растянутый на весь экран, перегружаемая функция позволяет разить экран на несколько областей и к каждой приемнить свою текстуру. Функции Create*Texture* создают текстуры заданного формата. Функции CreateShader/DeleteShader соответственно создают и удаляют шейдерную программу.

Структура демо у нас будет такая – функция инициализации InitFBO вызывается в момент создания формы и отвечает за создание и инициализацию всех буферов, функция FreeFBO - вызывается в момент завершения работы приложения и отвечает за освобождение видеопамяти. Весь процесс рисования, включая рендеринг в текстуру, будет производиться в функции FBORender.

Давайте рассмотрим каждую из них. Начнем с процедуры инициализации – в простейшем случае наша задача создать буфер кадра и прикрепить к нему текстуру, в которую и будет производиться рендеринг. Для этого объявим несколько глобальных переменных типа GLUIint, в TextureId будет хранится идентификатор текстуры, в fboId – идентификатор буфера кадра.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
procedure InitFBO(Width, Height: integer);
var Status: GLUInt;
begin
  //Для начала создадим текстуру куда все это будет рендериться
  textureId:=CreateTextureRGBA8(Width, Height, true);

  //Создадим буфер FBO
  glGenFramebuffersEXT(1, @fboId);
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboId);

  //Буферы созданы, осталось лишь присоеденить текстуру к FBO
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
                            GL_TEXTURE_2D, textureId, 0);
  //Проверим статус, чтоб убедиться что нет никаких ошибок
  Status:=glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
  assert(status=GL_FRAMEBUFFER_COMPLETE_EXT, 'FBOError '+inttostr(Status));
  //Ну и не забудем деактивировать буфер, иначе весь последующий рендеринг
  //будет осуществляться в него
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
end;


Итак, разберем еще раз что мы сделали – в строке 5 мы воспользовались одной из функцией нашего DemoTools и создали целочисленную текстуру с форматом GL_RGBA8 с разрешением Width x Height и разрешили автоматической генерацию мип-уровней для нее. Далее в строках 8-12 мы создали буфер FBO и присоединили к нему текстуру в качестве буфера цвета с таргетом GL_COLOR_ATTACHMENT0_EXT.

После того как буфер создан – в строках 15-16 проверяем статус буфера кадра, чтоб убедиться в отсутствии ошибок. Ну и делаем данный буфер неактивным строкой 19, так как иначе весь последующий рендеринг будет осуществляться в этот буфер.

Сразу же напишем процедуру удаления данного буфера а так же сгенерированных нами текстур:


1
2
3
4
5
6
7
procedure FreeFBO;
begin
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
  glBindTexture(GL_TEXTURE_2D, 0);
  glDeleteTextures(1, @TextureId);
  glDeleteFramebuffersEXT ( 1, @fboId );
end;

Теперь основная часть – процесс рендеринга в текстуру:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
procedure FBORender;
begin
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboId); //Активируем FBO
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);//Очищаем буферы цвета и глубины
  DrawCube; //Рендерим объект/сцену в текстуру
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);//Деактивируем FBO
  //Сгенерируем для полученной текстуры мип-уровни
  glBindTexture(GL_TEXTURE_2D, textureId);
  glGenerateMipmapEXT(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, 0);//Деактивируем текстуру
  drawScreenQuad(textureId); //рисуем скринквад с полученной текстурой
end;

Разберем данную процедуру – прежде чем осуществлять рендеринг нам необходимо сделать наш буфер кадра активным, что мы и делаем в строке 3, после того как мы перенаправили весь рендеринг в нашу текстуру, нужно очистить старое изображение в текстуре, для этого как и при обычном рендеринге, мы в строке 4 вызываем функцию glClear. После того как мы очистили буфер цвета и буфер глубины самое время что-то в нем нарисовать. Здесь и далее мы будем рисовать кубик при помощи функции DrawCube из нашего вспомогательного модуля. Естественно что вместо кубика может быть произведено рисование любого объекта или даже целой сцены.

После того как объект/сцена отрендерены необходимо деактивировать буфер кадра, вернувшись к стандартному оконному контексту. Делается это в строке 6. В случае если эта текстура будет накладываться на какой-то объект, то имеет смысл сгенерировать для нее мип-уровни, для этого служит специальная функция glGenerateMipmapEXT, появившаяся в этом же расширении. На этом рендеринг в текстуру можно считать завершенным. Чтоб увидеть результат наложим эту текстуру на какой-то объект, в примере это прямоугольник растянутый на весь экран, выводимый посредством функции drawScreenQuad из нашего вспомогательного модуля.

Осталось лишь в событие формы onCreate вызвать нашу функцию инициализации, передав в нее размер текстуры, которые у нас совпадает с размером вьюпорта и поместить вызов процедуры FBORender в цикл рисования.

Если вы все сделали правильно, то в результате вы должны получить результат как на Рис.1.
Рис.1. Простой пример рендеринга куба в текстуру.
Полный исходный код примера можно найти в FBOBase.


В этом примере мы привязались к размеру вьюпорта при создании FBO, а этот размер может меняться. Попытайтесь поизменять размер формы и понаблюдайте что происходит с изображением. Чтоб такое не происходило нам нужно перед активацией буфера FBO принудительно изменить размер вьюпорта, чтоб он соответствовал размеру, указанному для буфера кадра и восстановить исходный размер после того как рендеринг в текстуру будет завершен. Для этого нам понадобится несколько вспомогательных переменных – глобальные texWidth, texHeight – будут содержать размер текстуры/буфера кадра (мы ведь помним что эти размеры должны совпадать), и вектор Viewport, в которые будет сохраняться текущий размер вьюпорта. В этом случае процедура рендеринга немного изменится:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
procedure FBORender;
var viewport:array[0..3] of integer;
begin
  glGetIntegerv(GL_VIEWPORT, @viewport);
  glViewport(0,0,texWidth,texHeight);
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboId); //Активируем FBO
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);//Очищаем буферы цвета и глубины
  DrawCube; //Рендерим объект/сцену в текстуру
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);//Деактивируем FBO
  glViewport(viewport[0],viewport[1],viewport[2],viewport[3]);
  //Сгенерируем для полученной текстуры мип-уровни
  glBindTexture(GL_TEXTURE_2D, textureId);
  glGenerateMipmapEXT(GL_TEXTURE_2D);
  drawScreenQuad(textureId); //рисуем скринквад с полученной текстурой
  glBindTexture(GL_TEXTURE_2D, 0);//Деактивируем текстуру
end;

Не забудьте установить значения texWidth, texHeight, чтобы они соответствовали размеру буфера кадра. Запустите проект и попробуйте изменить размеры формы. Как видите, теперь текстура соответствует вьюпорту при любом размере формы, что нам и требовалось.

Как видите, ничего сложного в работе с FBO нет, вся логика работы повторяет логику работы с буферами VBO, за исключением нескольких новых команд, но сама технология на столько многогранна, что одним примером тут не обойдешься. Как вы уже наверно заметили, здесь мы не использовали буфер рендеринга, потому получить доступ к буферу глубины или буферу трафарета нам не удастся. Использование буфера глубины нужно для многих эффектов, потому в следующей части статьи мы с вами научимся его использовать и определим в чем отличие между буфером рендеринга и текстурой.

Комментариев нет:

Отправить комментарий