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

Основы FBO в OpenGL. Часть шестая, R2VB

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


В предыдущей части мы с вами немного познакомились с технологией PBO и узнали что при помощи нее как-то можно перенести данные в вершинный буфер (VBO). Простое копирование не представляет интереса для нас, потому рассмотрим более сложный случай - осуществление рендеринга прямо в вершинный буфер. Сокращенно эту технологию называют R2VB, что расшифровывается как  "Render to Vertex Buffer". 



Данная технология основана на уже известных нам VBO, FBO, MRT и PBO. Суть технологии заключается в том, чтоб посредством технологии FBO/MRT отрендерить какой-то объект (чаще всего скринквад) с примененной к нему текстурой содержащей какие-то данные, к примеру карту высот или коэффициенты функции или элементы матрицы или кадры анимации и т.д. Далее, в шейдере, мы читаем эти данные из текстуры и осуществляем их обработку. Результаты расчетов мы помещаем в буферы цвета. Далее мы переносим данные из буферов цвета в пиксельные буферы (PBO) при помощи рассмотренных в предыдущей главе способов. Ну и в дальнейшем при необходимости рендерим их, забиндив буфер PBO как VBO, изменив цель с GL_PIXEL_PACK_BUFFER на GL_ARRAY_BUFFER.

Процедура несколько громоздкая, но не очень сложная. Казалось бы зачем выполнять такое количество операций, если можно было бы сразу передать данные в буфер VBO? Дело в том, что одним из узких мест современных видеосистем является шина данных, которая имеет очень ограниченную пропускную способность. Мы об этом уже читали, когда разбирались с преимуществами VBO. Так же там было названо основной недостаток данной технологии – из-за ограниченной пропускной способности не желательно без особой необходимости в реальном времени изменять координаты вершин. Используя технологию r2vb мы можем обойти данное ограничение, переложив всю процедуру изменения координат вершин на фрагментный шейдер. После того как в фрагментном шейдере будут просчитаны координаты всех вершин, нам останется лишь скопировать данные из буфера цвета в вершинный буфер минуя CPU, так как копирование данных будет осуществляться внутри видеопамяти.

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

Данная технология чем-то похожа на технологию Vertex Texture Fetch (VTF) продвигаемую NVidia, но VTF работает только на видеокартах семейства NVidia и новых видеокартах ATI, что несколько ссужает возможности ее применения. Второй недостаток – она работает только в вершинном шейдере, что так же может сказаться на производительности старых видеокарт.

Еще одно немаловажное отличие - технология VTF может работать только с форматом RGBA32F (в других режимах драйвер переводит видео в софтварный режим), что бывает не совсем удобно. Так же в вершинном шейдере нам не доступен мипмаппинг для этих текстур, ну и используя технологию R2VB мы можем одновременно заполнить данными 4 буфера, из которых некоторые могут не меняться со временем, в то время как при использовании VTF мы вынуждены выполнять эти расчеты на каждом кадре. Так же в фрагментном шейдере мы имеем доступ к большему количеству текстурных сэмплов разных форматов.

Я надеюсь убедил вас в полезности данной технологии, потому достаточно теории и давайте рассмотрим один из примеров. Так как в этой технике объединены сразу множество функций (VBO, IBO, FBO, FBO, MRT, ReadBack), то даже простой пример будет огромным, потому вам придется запастись терпением.

Я остановил свой выбор на примере рельефных текстур. Как мы знаем, существует несколько способов выделить выступающие элементы на объекте, в частности это бамп-маппинг и параллакс-маппинг, идея которых заключается в том, что мы «обманываем» наблюдателя за счет игры света и тени и за счет небольшого сдвига текстурных координат. Увы, такой обман сразу же раскрывается если смотреть на объект по касательной, тогда видно что это всего лишь плоский объект с качественной текстурой. В DX11 и в OpenGL 4.x для этой задачи был создан специальный тесселятор, назначение которого как раз и состоит в придании рельефности поверхности за счет создания дополнительной геометрии по прилагаемой карте высот. Выглядит это конечно фантастически, если забыть о низком фпс и необходимости покупки новой видеокарты (а в случае DX11 еще и обновления операционной системы). Но не даром говорят – «все новое это хорошо забытое старое», вот мы и попробуем повторить эти эффекты воспользовавшись «старенькой» технологией r2vb, поддерживаемой большинством видеокарт низшей ценовой категории и откровенно устаревших.

Прежде чем перейти к коду, запишем  общий алгоритм использования R2VB:
  1. Создаем буфер PBO;
  2. Создаем буфер FBO и прикрепляем к нему буферы цветы;
  3. Активируем буфер FBO и осуществляем рендеринг в текстур;
  4. Биндим буфер PBO и при активном буфере FBO копируем данные из буфера цвета в буфер PBO;
  5. Деактивируем буферы PBO и FBO;
  6. Биндим буфер PBO с таргетом GL_ARRAY_BUFFER и осуществляем обычный рендеринг VBO.
Из особенностей - размер буфера кадра нужно подбирать таким, чтоб количество пикселей в нем было больше и равно количеству вершин в конечном буфере VBO. Лишние вершины мы можем отбросить уже на этапе рисования, указав в glDraw* в качестве количества вершин истинное значение, но тут нужно помнить что эти лишние вершины будут рассчитываться в фрагментном шейдере на равне с остальными.

Для примера я решил воспользоваться текстурой из NVSDK:
Рис.8. Текстура используемая в качестве карты высот
 У этой текстуры размер 512х512 пикселей, темные участки будут соответствовать нулевой высоте, светлые - максимальному выдавливанию. В примере я каждому пикселю текстуры сопоставил одну вершину буфера VBO, таким образом конечный буфер VBO будет состоять из примерно из 520 тысяч треугольников. На хранение каждой вершины нам потребуется 12 байт (float x,y,z), таким образом, для буфера PBO нам потребуется зарезервировать 512*512*12=3145728 байта. Так как кроме координат вершин нам потребуются еще нормали и текстурные координаты (так же по 12 байт на вершину), то создаем еще пару буферов PBO под них с таким же размером. Чтоб не писать все с нуля, возьмем за основу пример FBO_MRT и доработаем его. В первую очередь объявим несколько глобальных переменных под буферы PBO (Vertex, Normal иTexCoord соответственно):
    var pboIdv, pboIdn, pboIdt: GLUInt;
Так же добавим еще прочку идентификаторов текстур для буферов цвета. в которые будут помещены наши нормали и текстурные координаты:
    var NormalMap, TexCoordsMap: GLUInt;
Чтоб было меньше изменений, для хранения текстуры под координаты вершин я оставил текстуру с идентификатором TextureId. В результате процедура инициализации и удаления FBO примет такой вид:

 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
53
54
55
56
57
function CreatePBO(Size: integer): GLUInt;
begin
  glGenBuffers(1, @Result);
  glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, Result);
  glBufferData(GL_PIXEL_PACK_BUFFER_ARB, Size, nil, GL_DYNAMIC_DRAW);
end;

procedure InitFBO(Width, Height: integer);
var Status, PBOsize: GLUInt;
begin
  //Для начала создадим текстуру куда все это будет рендериться
  textureId:=CreateTextureRGB32F(Width, Height);
  NormalMap:=CreateTextureRGB32F(Width, Height);
  TexCoordsMap:=CreateTextureRGB32F(Width, Height);
  //Создадим буферы PBO для вершин, нормалей и текстурных координат
  PBOsize:=Width*Height*12;
  pboIdv:=CreatePBO(PBOsize);
  pboIdn:=CreatePBO(PBOsize);
  pboIdt:=CreatePBO(PBOsize);

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

  //Буферы созданы, осталось лишь присоеденить текстуру к FBO
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
                            GL_TEXTURE_2D, textureId, 0);
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT1_EXT,
                            GL_TEXTURE_2D, NormalMap, 0);
  glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT2_EXT,
                            GL_TEXTURE_2D, TexCoordsMap, 0);

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

procedure FreeFBO;
begin
  glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
  glBindTexture(GL_TEXTURE_2D, 0);
  glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);
  glDeleteTextures(1, @Textureid);
  glDeleteTextures(1, @NormalMap);
  glDeleteTextures(1, @TexCoordsMap);
  glDeleteFramebuffersEXT ( 1, @fboId );
  glBindBuffer(GL_PIXEL_PACK_BUFFER_ARB, 0);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
  glDeleteBuffers(1, @pboIdn);
  glDeleteBuffers(1, @pboIdv);
  glDeleteBuffers(1, @pboIdt);
  glDeleteBuffers(1, @iId);
end;

Функция CreatePBO просто создает буфер PBO с указанным размером, в ней нет ничего особенного, потому ее разбор пропущу.
В процедуре инициализации мы первым делом (в строках 12-14) создаем 3 пустых текстуры (расчет оптимального размера вьюпорта/текстуры/FBO мы рассмотрели выше). Далее, в строках 16-19, мы создаем 3 буфера PBO, используя ранее объявленную функцию "CreatePBO". Ну и в строках 26-31 прикрепляем созданные текстуры к нашему буферу кадра.

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

Все ключевые изменения у нас будут в процедуре PrepearePBO:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Эта процедура похожа на FBORender, с которой мы работали в предыдущих примерах, с той разницей, что эту процедуру мы будем вызывать только один раз, чтоб подготовить буферы VBO через рендеринг в текстуру. После того как мы с использованием MRT отрендерим скринквад в FBO (строки 10-16) мы осуществим копирование данных из буферов цвета в соответствующие буферы PBO (строки 18-28) и вызовем специальную процедуру "createIndexBufferStep", создающую индексный буфер для этой геометрии:
 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 createIndexBufferStep(step:integer=1);
var data: array of integer;
    i,j,k,w,h: integer;
begin
  w:=texWidth div step;
  h:=texHeight div step;
  trCount:=(w)*(h)*6;
  setlength(data,trCount);  k := 0;
  for j := 0 to h - 2 do
 for i := 0 to w - 2 do begin
            data [k]   := i*step + texWidth*j*step;
            data [k+1] := i*step + texWidth*j*step + step;
            data [k+2] := i*step + texWidth*j*step + texWidth*step;

            data [k+3] := i*step + texWidth*j*step + step;
            data [k+4] := i*step + texWidth*j*step + texWidth*step + step;
            data [k+5] := i*step + texWidth*j*step + texWidth*step;
            k:=k+6;
 end;
   glGenBuffers(1, @iId);
   glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iId);
   glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLUint) * trCount,
        @data[0], GL_STATIC_DRAW);
   glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
end;

Если вы знакомы с VBO, то здесь для вас нет ничего нового, тут просто генерируется индексный буфер создающий регулярную сетку из треугольников. Единственная особенность - мы указываем с каким шагом (step) генерировать эту сетку, чтоб получить возможность управлять детализацией.

Ну и наконец напишем нашу процедуру рисования полученного буфера VBO:
 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
procedure RenderVBO(wireframe: boolean = false);
begin
  glEnableClientState(GL_VERTEX_ARRAY);
  glEnableClientState(GL_NORMAL_ARRAY);
  glEnableClientState(GL_TEXTURE_COORD_ARRAY);

  glBindBuffer(GL_ARRAY_BUFFER, pboIdn);
  glNormalPointer(GL_FLOAT, 0, nil);

  glClientActiveTexture(GL_TEXTURE0);
  glBindBuffer(GL_ARRAY_BUFFER, pboIdt);
  glTexCoordPointer(3, GL_FLOAT, 0, nil);
  glBindBuffer(GL_ARRAY_BUFFER, pboIdv);
  glVertexPointer(3, GL_FLOAT, 0, nil);

  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, iId);

  if wireframe then
    glDrawElements(GL_LINES, TrCount, GL_UNSIGNED_INT, nil)
  else
    glDrawElements(GL_TRIANGLES, TrCount, GL_UNSIGNED_INT, nil);

  glDisableClientState(GL_VERTEX_ARRAY);
  glDisableClientState(GL_NORMAL_ARRAY);
  glDisableClientState(GL_TEXTURE_COORD_ARRAY);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
end;

Здесь для нас так же нет ничего нового, обычное рисование буфера VBO. Единственное - я добавил условие на "wireframe", это позволит в отладочном режиме увидеть нашу сетку. Почему использовал рисование с типом GL_LINES вместо указания типа полигона - такой подход дает немного более высокий фпс, хотя геометрия при этом может слегка искажаться (вместо треугольников получаются параллелепипеды).

Вся логика формирования буфера VBO (координаты вершин, нормали и текстурные координаты) у нас возложена на фрагментный шейдер, он имеет примерно такой вид:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
uniform sampler2D HeightMap;
uniform float scalefactor = 0.0;
uniform float invtexsize;
varying vec2 TexCoord;

void main(void)
{
  vec4 h = texture2D(HeightMap, TexCoord);
  vec4 h2 = texture2D(HeightMap, vec2(TexCoord.s,TexCoord.t+invtexsize)); 
  vec4 h3 = texture2D(HeightMap, TexCoord+vec2(invtexsize,invtexsize)); 
       h.xy=vec2(0.0,0.0);
       h2.xy=vec2(0.0,1.0);
       h3.xy=vec2(1.0,1.0);
  vec4 d1, d2;
       d1=h2-h;
       d2=h3-h;
  vec3 n = cross(d1.xyz,d2.xyz);         
  vec3 pos;
       pos.xy = (TexCoord.st - vec2(0.5,0.5))*2.0;
       pos.z = h.z*scalefactor;
  gl_FragData[0] = vec4(pos,1.0);
  gl_FragData[1] = vec4(-n,1.0);
  gl_FragData[2] = vec4(TexCoord,0.0, 0.0);
}

В HeightMap у нас передается текстура с картой высот, по высоте в трех соседних пикселях мы вычисляем нормаль как векторное произведение, координаты точки берем из текстурных координат квада, отцентровав их. В качестве текстурных координат берем просто текстурныее координаты квада. Все это помещается в 3 буфера цвета через gl_FragData. В invtexsize мы передаем размер одного пикселя, необходимо при выборе ближайшей точки для расчета нормали, scalefactor у нас задает глубину "выдавливания" вершин, при 0 мы получим плоский объект.


Ну вот и все, теперь осталось собрать это все в цикле рисования:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
procedure TForm1.GLDirectOpenGL1Render(Sender: TObject;
  var rci: TRenderContextInfo);
begin
  if not pboReady then begin
    HeightTex:=matlib.Materials[0].Material.Texture.Handle; 
    PrepearePBO;
  end;
  matlib.Materials[0].Apply(rci);
  RenderVBO(wireframe);
  matlib.Materials[0].UnApply(rci);
end;

Так как в качестве фрэймвока для демо я использую GLScene, то эту процедуру я поместил в обработчик события "GLDirectOpenGL.onRender", так же, чтоб не нагромождать и без того сложный код лишними процедурами, я воспользовался встроенными средствами для работы с текстурами (загрузка и применение, строки 5,8 и 10).

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

Так же для наглядности я добавил перестройку вершинного буфера, для этого в обработчик события TTrackBar я добавил следующий код:
   glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
   glDeleteBuffers(1, @iId);
   createIndexBufferStep(TrackBar1.Position);
Тоесть просто удаляется существующий индексный буфер и создается новый с новой детализацией.

В результате проделанной работы мы имеем такую вот картинку с честным ReliefMapping или аналогом аппаратной тесселяции на новых видеокартах:
Рис.9. Результат рендеринга в вершинный буфер.
Полные исходные коды примера вы найдете в R2VB.

На этом краткий курс работы с FBO подошел к концу, осталось привести вам список литературы по данной тематике: Ссылки по теме

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

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