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

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

Часть первая, немного теории
Часть вторая, простой пример
Часть третья, буфер глубины
Часть четвертая, MRT
Часть пятая, чтение
Часть шестая, R2VB
Ссылки по теме    
Используя FBO мы получаем в руки мощнейший инструмент по работе с эффектами, это и многопроходные рендеринги, пост-эффекты, тени, множественные источники света и многое многое другое. Но этим возможности FBO не ограничиваются. В предыдущих главах мы с вами разобрали как осуществить рендеринг (запись) в текстуру, в этой части статьи мы разберем еще одну важную особенность FBO – чтение из текстур и буферов в системную память, а так же немного познакомимся с технологией PBO (Pixel Buffer Object), которая откроет перед нами новые горизонты и размоет грань между текстурой и вершинным буфером, что откроет перед нами такие перспективные технологии как рендеринг в текстуру, скелетная анимация на GPU, отладка шейдера, математические многопоточные расчеты на GPU (GPGPU) и многое другое, но обо всем по порядку.


Cуществует два способа – прочесть данные напрямую из буфера вывода и скопировать эти данные из активной текстуры, разберем оба варианта.

Для начала разберем случай с копированием данных из текстуры, как более простой и возможно знакомый вам из опыта работы с текстурами. За основу возьмем пример (FBO_DB), как наиболее простой, лишь немного доработаем его – разделим окно на две части, в левой части окна будет находится вьюпорт, куда будет рендериться сцена, в правой части окна расположим обычный TImage, в который на стороне CPU будем рисовать данные из прочтенного буфера. Процедуру FBOInit оставляем без изменений, а вот над процедурой рендеринга нам предстоит поработать:
 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
34
35
36
37
38
39
procedure FBORender;
var viewport:array[0..3] of integer;
    t: pointer;
    p: ^byte;
    size: integer;
    i,j: integer;
    sl: PByteArray;
begin
  size:=texWidth*texHeight*4;
  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); //рисуем скринквад с полученной текстурой
  getmem(t,size);
  glEnable(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, textureId);
  glGetTexImage(GL_TEXTURE_2D, 0, GL_BGRA, GL_UNSIGNED_BYTE, t);
  glBindTexture(GL_TEXTURE_2D, 0); p:=t;
  glDisable(GL_TEXTURE_2D);
    for i:=texHeight-1 downto 0 do begin
     sl:=bmp.ScanLine[i];
     for j:=0 to texWidth-1 do begin
         sl[j*3+0]:=p^; inc(p);
         sl[j*3+1]:=p^; inc(p);
         sl[j*3+2]:=p^; inc(p);
         inc(p);
     end;
  end;
  image1.Picture.Bitmap.Assign(bmp);
  Freemem(t,size);
  glBindTexture(GL_TEXTURE_2D, 0);//Деактивируем текстуру
end;

Изменений много, но если исключить работу с битмапом, то останется лишь одна новая строка: glGetTexImage(GL_TEXTURE_2D, 0, GL_BGRA, GL_UNSIGNED_BYTE, t);

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

Единственное на что нужно обратить внимание, так это на формат возвращаемой текстуры – для правильной цветопередачи нужно читать текстуру с форматом GL_BGRA а не GL_RGBA.


Если вы все сделали правильно, то в результате у вас в обоих половинах экрана должна быть одинаковая картинка, как на рисунке ниже:
Рис.5. Чтение текстуры через glGetTexImage
В случае использование нескольких буферов цвета суть алгоритма не меняется, просто перед чтением биндим нужный текстурный юнит (строки 22-26).

Полный исходный код вы найдете в FBO_RT.

Второй способ чтения из текстуры предполагает чтение напрямую из буфера вывода и позволяет производить чтение в том числе и из буферов глубины и трафарета. Для того чтоб воспользоваться данным способом необходимо немного переработать функцию рендеринга. За основу возьмем пример FBO_MRT, чтоб показать как можно указывать буфер чтения:
 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
procedure FBORender;
var viewport:array[0..3] of integer;
    t: pointer;
    p: ^byte;
    size: integer;
    i,j: integer;
    sl: PByteArray;
const
  buffers: array [0..3] of GLenum =
      (GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT,
       GL_COLOR_ATTACHMENT2_EXT, GL_COLOR_ATTACHMENT3_EXT);
begin
  size:=texWidth*texHeight*4; getmem(t,size);
  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); // Деактивируем шейдер
  //Копируем буфер в системную память
  glReadBuffer(GL_COLOR_ATTACHMENT3_EXT);
  glReadPixels(0,0, texWidth, texHeight,GL_BGRA, GL_UNSIGNED_BYTE, t);
  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);//Деактивируем текстуру
  p:=t;//сохраняем указатель на начало буфера
  //Рисуем данные из буфера на канвасе
  for i:=texHeight-1 downto 0 do begin
     sl:=bmp.ScanLine[i];
     for j:=0 to texWidth-1 do begin
         sl[j*3+0]:=p^; inc(p);
         sl[j*3+1]:=p^; inc(p);
         sl[j*3+2]:=p^; inc(p);
         inc(p);
     end;
  end;
  image1.Picture.Bitmap.Assign(bmp);
  Freemem(t,size);
end;

Как видите, принципиальных отличий не так уж и много. Основное отличие от предыдущего примера заключается в указании буфера чтения, перед тем как будет осуществлено чтение из буфера (строки 24-25). Особенность в том, что в момент установки буфера чтения и собственно чтения из буфера, FBO должно быть активным, иначе получим ошибку о неизвестном типе перечисления, так как цель GL_COLOR_ATTACHMENT0_EXT допускается использовать только при активном FBO.

Запустив пример мы увидим примерно то же что и на рисунке ниже:
Рис.6. Чтение из буфера цвета через glReadPixels
Исходные коды примера вы найдете в MRT_RB.

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

Теперь рассмотрим более сложный случай, но более функциональный и местами более эффективный. Данная методика основана на технологии PBO, суть которой заключается в том, что мы можем обращаться к любому буферу в видеопамяти так же как и к буферу VBO, изменив лишь цель с GL_ARRAY_BUFFER на GL_PIXEL_PACK_BUFFER_ARB/ GL_PIXEL_UNPACK_BUFFER_ARB. Вдаваться в подробности данной технологии я не стану, так как это тема отдельной статьи, здесь лишь покажу как это можно использоваться для чтения/записи в текстуру.

За основу возьмем предыдущий пример, изменения у нас коснутся всех функций, хоть и незначительные. В функции инициализации нам нужно создать буфер PBO:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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);
  //Создадим буфер PBO
  glGenBuffers(1, @pboId);
  glBindBuffer(GL_PIXEL_PACK_BUFFER, pboId);
  glBufferData(GL_PIXEL_PACK_BUFFER, width*height*4, nil, GL_STREAM_READ);
  //Буферы созданы, осталось лишь присоединить текстуры к 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;

Здесь в строках 10-13 создается нужный нам буфер PBO с идентификатором pboId: GLUInt, после создания буфера мы по аналогии с VBO биндим его, прежде чем загрузить в него какие-то данные (строка 12). Как уже было сказано – основное отличие PBO от VBO – в типе буфера, так для в PBO у нас используется цель - GL_PIXEL_PACK_BUFFER_ARB, если нужно осуществить чтение или GL_PIXEL_UNPACK_BUFFER_ARB в случае записи данных в буфер. В нашем примере мы будем читать данные из буфера, потому выбираем первый вариант, так же об этом говорит константа GL_STREAM_READ. Размер буфера мы выбираем из расчета количества пикселей и типа буфера, в нашем примере используется целочисленная 32 битная текстура, потому размер данных будет width*height*4.

Никаких других изменений в процедуру инициализации вносить не нужно.

Так же при завершении работы нам необходимо освободить буфер PBO. Для этого допишем одну строку в процедуру FreeFBO:
1
2
3
4
5
6
7
8
9
procedure FreeFBO;
begin
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
  glBindTexture(GL_TEXTURE_2D, 0);
  glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);
  glDeleteTextures(4, @Texturesid);
  glDeleteFramebuffersEXT ( 1, @fboId );
  glDeleteBuffers(1, @pboId);
end;

Теперь переходим к процедуре рендеринга:
 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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
procedure FBORender;
var viewport:array[0..3] of integer;
    t: pointer;
    i,j: integer;
    sl: PByteArray;
type
    TByteArray = array of byte;
const
  buffers: array [0..3] of GLenum =
      (GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT,
       GL_COLOR_ATTACHMENT2_EXT, GL_COLOR_ATTACHMENT3_EXT);
begin
  size:=texWidth*texHeight*4;
  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); // Деактивируем шейдер
  //Указываем буфер для чтения и производим чтение из него
  glReadBuffer(GL_COLOR_ATTACHMENT3_EXT);
  glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, pboId);
  glReadPixels(0,0, texWidth, texHeight,GL_BGRA, GL_UNSIGNED_BYTE, nil);
  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);//Деактивируем текстуру
  glBindBuffer(GL_PIXEL_PACK_BUFFER, pboId);
  t:=glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
  for i:=texHeight-1 downto 0 do begin
     sl:=bmp.ScanLine[i];
     for j:=0 to texWidth-1 do begin
         sl[j*3+0]:=TByteArray(t)[(texHeight-1-i)*texWidth*4+j*4+0];
         sl[j*3+1]:=TByteArray(t)[(texHeight-1-i)*texWidth*4+j*4+1];
         sl[j*3+2]:=TByteArray(t)[(texHeight-1-i)*texWidth*4+j*4+2];
     end;
  end;
  glUnmapBufferARB(GL_PIXEL_PACK_BUFFER);
  glBindBufferARB(GL_PIXEL_PACK_BUFFER, 0);
  image1.Picture.Bitmap.Assign(bmp);
end;

Принципиальных отличий у нас здесь несколько, во-первых - прежде чем читать данные из буфера цвета мы забиндили буфер PBO (строка 25), во-вторых – в качестве указателя на область памяти куда будут считываться данные (строка 26) мы указали nil, дело в том, что после того как мы забиндили буфер PBO, указатель в функции glReadPixels интерпретируется как смещение от начала забинденного буфера.

После этой процедуры в буфере pboId будут содержатся прочитанные нами данные и все что нам останется – в нужном месте осуществить чтение данных из этого буфера посредством функций знакомых нам из VBO (строки 39-49). Для этого мы просто отображаем область видеопамяти в системную область памяти посредством glMapBuffer, и  отрисовываем эти данные на канвасе (строки 44-46). После того как все данные прочитаны необходимо освободить указатель (строка 49) и деактивировать буфер PBO (строка 50).

Результат ничем не отличается от предыдущего примера, но при этом мы имеем более высокий фпс:
Рис.7. Чтение из буфера цвета через PBO
Полный исходный код данного примера вы найдете в MRT_RPBO.

Использование технологии PBO не ограничивается чтением/записью в текстуру, хотя и позволяет существенно ускорить этот процесс (более подробно это будет рассмотрено в другой статье), вся сила этой технологии раскрывается в возможности осуществлять копирование данных из текстуры в буфер VBO на стороне GPU, без промежуточного копирования в системную память, благодаря чему такое копирование осуществляется очень быстро и может быть осуществлено в реальном времени. Совместно с FBO такая технология называется рендерингом в вершинный буфер, или сокращенно R2VB. Один из примеров ее использования мы разберем в следующей части статьи.

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

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