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

Основы FBO в OpenGL. Часть четвертая, MRT

Часть первая, немного теории
Часть вторая, простой пример
Часть третья, буфер глубины
Часть четвертая, MRT
Часть пятая, чтение
Часть шестая, R2VB
Ссылки по теме

В предыдущих частях мы с вами научились создавать буфер FBO, прикреплять к нему буферы рендеринга и текстуры, а так же попробовали разные комбинации этих буферов, но этим технология FBO не ограничивается. Одной из ключевых возможностей FBO является возможность прикрепить к нему несколько буферов цвета и одновременно рисовать в каждый из них. Данная технология носит название MRT (Multiple Render Target). Разберем эту технологию на примере демо, осуществляющего вывод каждого канала цвета в отдельную текстуру.


Запись данных в эти буферы возможна только из фрагментного шейдера, посредством команды GLSL gl_FragData[n], где n – номер буфера цвета.

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

Для начала напишем шейдеры, вершинный:
1
2
3
4
void main ()
{
  gl_Position = ftransform();  
}

И фрагментный:

1
2
3
4
5
6
7
void main(void)
{
  gl_FragData[0] = vec4(1.0,0.0,0.0,1.0);
  gl_FragData[1] = vec4(0.0,1.0,0.0,1.0);
  gl_FragData[2] = vec4(0.0,0.0,1.0,1.0);
  gl_FragData[3] = vec4(gl_FragCoord.zzz,1.0);
}

С вершинным шейдером все понятно, остановимся на фрагментном. Здесь в первых 3 текстурных юнита будут помещены цветовые компоненты (красная, зеленая и синя соответственно), а в 4-й буфер будет помещена нормированная координата z фрагмента, переведенная в серый цвет.
На этом с шейдерами закончили, переходим к процедуре инициализации FBO. Итак, нам потребуется создать 4 текстурных юнита, задать для каждого параметры и форматы, активировать генерацию мип-уровней и присоединить каждый из них к буферу FBO. Дабы сократить объем кода оформим это все в виде массивов и циклов. Для начала объявим глобальный массив:
  TexturesId: array[0..3] of GLUInt;
в котором и будут храниться идентификаторы наших текстур. Теперь перепишем процедуру инициализации:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
procedure InitFBO(Width, Height: integer);
var Status: GLUInt;
    i: integer;
begin 
  //Создадим 4 целочисленные текстуры
  for i:=0 to 3 do TexturesId[i]:= CreateTextureRGBA8(Width, Height, true);
       
  // Создадим буфер FBO
  glGenFramebuffersEXT(1, @fboId);
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboId);

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

Как видите? практически ничего не изменилось, за исключением того, что мы теперь указываем какая текстура к какому буферу цвета будет прикрепляться. Буфер рендеринга мы здесь не используем.

Более существенные изменения коснутся процедуры FBORender, вот ее полный код:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
procedure FBORender;
var viewport: array[0..3] of integer;
    i: integer;
const
  buffers: array [0..3] of GLenum =
      (GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT,
       GL_COLOR_ATTACHMENT2_EXT, GL_COLOR_ATTACHMENT3_EXT);
begin
  glGetIntegerv(GL_VIEWPORT, @viewport);
  glViewport(0,0,texWidth,texHeight);
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboId); //Активируем FBO
  //Указываем в какие буферы будет происходить рисование
  glDrawBuffers (4, @buffers[0]);
  glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);//Очищаем буферы цвета и глубины
  glUseProgram(Shader.pId); //Активируем шейдер
    DrawCube; //Рендерим объект/сцену 
  glUseProgram(0); // Деактивируем шейдер
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);//Деактивируем FBO
  //Восстанавливаем рисование в фоновый буфер
  glReadBuffer(GL_BACK); glDrawBuffer(GL_BACK);
  glViewport(viewport[0],viewport[1],viewport[2],viewport[3]);
  //Сгенерируем для полученных текстур мип-уровни
  for i:=0 to 3 do begin
    glBindTexture(GL_TEXTURE_2D, texturesId[i]);
    glGenerateMipmapEXT(GL_TEXTURE_2D);
  end;
  //Нарисуем четыре квада с разными текстурами
  drawScreenQuad(2,2,[texturesId[0], texturesId[1], texturesId[2], texturesId[3]]);
  glBindTexture(GL_TEXTURE_2D, 0);//Деактивируем текстуру
end;

С генерацией мип-уровней думаю все понятно(строки 22-26), давайте разберем что же такое этот glDrawBuffers (строка 13) и зачем мы его используем. В предыдущих примерах мы использовали лишь один буфер цвета, в который и происходило рисование (или же вообще отключали его, как в примере с буфером глубины). Сейчас же нам нужно произвести рисование сразу в 4 буфера, об этом мы и должны как-то сообщить OpenGL, для этих целей служит функция glDrawBuffers, первым параметром которой идет количество цветовых буферов в которые мы будем осуществлять рендеринг, а вторым – указатель на список идентификаторов буферов, в нашем случае массив buffers(строки 4-7).

После завершения рендеринга в текстуру желательно восстановить буферы вывода, это делается в строке 20.

Кроме этого нам еще нужно применить шейдер к рисованию нашего кубика (строка 15) и отменить шейдер после завершения рисования (строка 17).

Для создания шейдера воспользуемся функцией CreateShader из модуля DemoTools, в качестве параметров этой функции используются имена файлов содержащие код вершинного и фрагментного шейдеров. Результат сохраняется в глобальной переменной Shader: TShader, тип TShader так же определен в нашем вспомогательном модуле.
 Если вы все сделали правильно то у вас должно получиться примерно как на Рис.3.
Рис.3. Рендеринг в 4 буфера цвета.

Полный код примера можно найти в FBO_MRT.

Присоединение буфера глубины и буфера трафарета происходит аналогичным образом, потому останавливаться на этом не будем.

Одно важное замечание – формат, тип и размер всех буферов цвета должен совпадать.

Это ограничение можно обойти, создав несколько буферов FBO и сделав часть буферов разделяемыми (к примеру буфер глубины будет общим для всех буферов кадра), но в этом случае рендеринг будет многопроходным – вначале биндим буфер кадра с присоединенным к нему буфером глубины и осуществляем рендеринг в буфер глубины, потом присоединяем буфер кадра с присоединенными текстурой первого типа (к примеру Float) и буфером глубины (не забываем что он у нас общий для всех буферов кадра) и осуществляем второй проход рендеринга, после чего биндим третий буфер кадра и осуществляем 3-й проход рендеринга. Громоздко, но иногда это необходимо.
Попробуем это реализовать на примере, для этого в очередной раз перепишем процедуры инициализации и рендеринга:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
procedure InitFBO(Width, Height: integer);
begin 
  fixedColor:=CreateTextureRGBA8(Width, Height, true);
  floatColor:=CreateTextureRGBA16F(Width, Height, true); 
  //Создаём буфер глубины.
  glGenRenderbuffersEXT(1, @depth);
  glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depth);
  glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT24, width, height);
  glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);
  //Создадим три буфера кадра
  glGenFramebuffersEXT(3, @framebuffers);
  //Буферы созданы, осталось лишь присоединить текстуры к FBO, при этом
  //буфер глубины общий для всех
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffers[0]);
  glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, depth);

  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffers[1]);
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, fixedColor, 0);
  glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, depth);

  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffers[2]);
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, floatColor, 0);
  glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, depth);
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
end;

Изменений очень много, потому давайте разберем все по порядку.

Начнем с имен переменных – здесь мы используем две текстуры – целочисленную и вещественную (float) текстуру с именами fixedColor и floatColor соответственно. Так же нам потребуется идентификатор для буфера глубины, мы его обозвали depth. Так же в нашем примере используется три буфера кадра, потому мы оформили их в виде массива:
framebuffers: array[0..2] of GLUInt, который заполняется в строке 11.

Далее в строках 10-23 мы создаем три различных комбинации буферов – только глубина (14-15), глубина + целочисленная текстура(17-19), глубина + флоат-текстура(21-23).

Так как у нас 3 буфера кадра, то в идеале нам нужно осуществлять проверку статуса после заполнения каждого из них, но для экономии места мы эту часть пропустим.

Ну и процедура рендеринга:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
procedure FBORender;
var viewport:array[0..3] of integer;
begin
  glGetIntegerv(GL_VIEWPORT, @viewport);
  glViewport(0,0,texWidth,texHeight);
  //Рисуем в буфер глубины
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffers[0]); 
    glDrawBuffer(GL_NONE);
    glClear(GL_DEPTH_BUFFER_BIT);//Очищаем буфер глубины
    DrawCube; 
  //Рисуем в целочисленную текстуру
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffers[1]);
    glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);
    glClear ( GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
    DrawCube; 
  //Рисуем в float-текстуру
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffers[2]);
    glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);
    glClear ( GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT);
    DrawCube; 
  // Возврат к оконному контексту.
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);//Деактивируем FBO
  glDrawBuffer(GL_BACK);
  glViewport(viewport[0],viewport[1],viewport[2],viewport[3]);
  //генерируем мип-уровни 
  glBindTexture(GL_TEXTURE_2D, fixedColor);
  glGenerateMipmapEXT(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, floatColor);
  glGenerateMipmapEXT(GL_TEXTURE_2D);
  //Нарисуем два квада с разными текстурами
  drawScreenQuad(1,2,[ fixedColor, floatColor]);
  glBindTexture(GL_TEXTURE_2D, 0);//Деактивируем текстуру
end;


Основное отличие от предыдущих примеров – нам необходимо трижды отрисовать наш куб в 3 буфера кадров, что мы и делаем в строках 7-10, потом в строках 12-15, и наконец в 17-20.
Результат здесь не очень эффектный, но нам важно чтоб оно вообще рисовало без ошибок:
Рис.4. Многопроходный рендеринг с разделяемым буфером глубины.
Полные исходные коды примера вы найдете в FBO_3T.

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

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

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