Часть первая, немного теории
Часть вторая, простой пример
Часть третья, буфер глубины
Часть четвертая, MRT
Часть пятая, чтение
Часть шестая, R2VB
Ссылки по теме
Используя FBO мы получаем в руки мощнейший инструмент по работе с эффектами, это и многопроходные рендеринги, пост-эффекты, тени, множественные источники света и многое многое другое. Но этим возможности FBO не ограничиваются. В предыдущих главах мы с вами разобрали как осуществить рендеринг (запись) в текстуру, в этой части статьи мы разберем еще одну важную особенность FBO – чтение из текстур и буферов в системную память, а так же немного познакомимся с технологией PBO (Pixel Buffer Object), которая откроет перед нами новые горизонты и размоет грань между текстурой и вершинным буфером, что откроет перед нами такие перспективные технологии как рендеринг в текстуру, скелетная анимация на GPU, отладка шейдера, математические многопоточные расчеты на GPU (GPGPU) и многое другое, но обо всем по порядку.
Cуществует два способа – прочесть данные напрямую из буфера вывода и скопировать эти данные из активной текстуры, разберем оба варианта.
Для начала разберем случай с копированием данных из текстуры, как более простой и возможно знакомый вам из опыта работы с текстурами. За основу возьмем пример (FBO_DB), как наиболее простой, лишь немного доработаем его – разделим окно на две части, в левой части окна будет находится вьюпорт, куда будет рендериться сцена, в правой части окна расположим обычный TImage, в который на стороне CPU будем рисовать данные из прочтенного буфера. Процедуру FBOInit оставляем без изменений, а вот над процедурой рендеринга нам предстоит поработать:
Изменений много, но если исключить работу с битмапом, то останется лишь одна новая строка: glGetTexImage(GL_TEXTURE_2D, 0, GL_BGRA, GL_UNSIGNED_BYTE, t);
Эта функция копирует активную текстуру в системную память с указанным форматом, далее, для визуального сравнения с оригиналом, мы просто отрисовали ее на канвасе.
Единственное на что нужно обратить внимание, так это на формат возвращаемой текстуры – для правильной цветопередачи нужно читать текстуру с форматом GL_BGRA а не GL_RGBA.
Если вы все сделали правильно, то в результате у вас в обоих половинах экрана должна быть одинаковая картинка, как на рисунке ниже:
В случае использование нескольких буферов цвета суть алгоритма не меняется, просто перед чтением биндим нужный текстурный юнит (строки 22-26).
Полный исходный код вы найдете в FBO_RT.
Второй способ чтения из текстуры предполагает чтение напрямую из буфера вывода и позволяет производить чтение в том числе и из буферов глубины и трафарета. Для того чтоб воспользоваться данным способом необходимо немного переработать функцию рендеринга. За основу возьмем пример FBO_MRT, чтоб показать как можно указывать буфер чтения:
Как видите, принципиальных отличий не так уж и много. Основное отличие от предыдущего примера заключается в указании буфера чтения, перед тем как будет осуществлено чтение из буфера (строки 24-25). Особенность в том, что в момент установки буфера чтения и собственно чтения из буфера, FBO должно быть активным, иначе получим ошибку о неизвестном типе перечисления, так как цель GL_COLOR_ATTACHMENT0_EXT допускается использовать только при активном FBO.
Запустив пример мы увидим примерно то же что и на рисунке ниже:
Исходные коды примера вы найдете в MRT_RB.
Здесь мы осуществили чтение из буфера GL_COLOR_ATTACHMENT3_EXT, аналогичным образом можно прочесть даны из любого присоединенного буфера, включая буфер глубины и буфер трафарета.
Теперь рассмотрим более сложный случай, но более функциональный и местами более эффективный. Данная методика основана на технологии PBO, суть которой заключается в том, что мы можем обращаться к любому буферу в видеопамяти так же как и к буферу VBO, изменив лишь цель с GL_ARRAY_BUFFER на GL_PIXEL_PACK_BUFFER_ARB/ GL_PIXEL_UNPACK_BUFFER_ARB. Вдаваться в подробности данной технологии я не стану, так как это тема отдельной статьи, здесь лишь покажу как это можно использоваться для чтения/записи в текстуру.
За основу возьмем предыдущий пример, изменения у нас коснутся всех функций, хоть и незначительные. В функции инициализации нам нужно создать буфер PBO:
Здесь в строках 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:
Теперь переходим к процедуре рендеринга:
Принципиальных отличий у нас здесь несколько, во-первых - прежде чем читать данные из буфера цвета мы забиндили буфер PBO (строка 25), во-вторых – в качестве указателя на область памяти куда будут считываться данные (строка 26) мы указали nil, дело в том, что после того как мы забиндили буфер PBO, указатель в функции glReadPixels интерпретируется как смещение от начала забинденного буфера.
После этой процедуры в буфере pboId будут содержатся прочитанные нами данные и все что нам останется – в нужном месте осуществить чтение данных из этого буфера посредством функций знакомых нам из VBO (строки 39-49). Для этого мы просто отображаем область видеопамяти в системную область памяти посредством glMapBuffer, и отрисовываем эти данные на канвасе (строки 44-46). После того как все данные прочитаны необходимо освободить указатель (строка 49) и деактивировать буфер PBO (строка 50).
Результат ничем не отличается от предыдущего примера, но при этом мы имеем более высокий фпс:
Полный исходный код данного примера вы найдете в MRT_RPBO.
Использование технологии PBO не ограничивается чтением/записью в текстуру, хотя и позволяет существенно ускорить этот процесс (более подробно это будет рассмотрено в другой статье), вся сила этой технологии раскрывается в возможности осуществлять копирование данных из текстуры в буфер VBO на стороне GPU, без промежуточного копирования в системную память, благодаря чему такое копирование осуществляется очень быстро и может быть осуществлено в реальном времени. Совместно с FBO такая технология называется рендерингом в вершинный буфер, или сокращенно R2VB. Один из примеров ее использования мы разберем в следующей части статьи.
Часть вторая, простой пример
Часть третья, буфер глубины
Часть четвертая, 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 |
Полный исходный код вы найдете в 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 |
Здесь мы осуществили чтение из буфера 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 |
Использование технологии PBO не ограничивается чтением/записью в текстуру, хотя и позволяет существенно ускорить этот процесс (более подробно это будет рассмотрено в другой статье), вся сила этой технологии раскрывается в возможности осуществлять копирование данных из текстуры в буфер VBO на стороне GPU, без промежуточного копирования в системную память, благодаря чему такое копирование осуществляется очень быстро и может быть осуществлено в реальном времени. Совместно с FBO такая технология называется рендерингом в вершинный буфер, или сокращенно R2VB. Один из примеров ее использования мы разберем в следующей части статьи.
Комментариев нет:
Отправить комментарий