Главная Статьи Ссылки Скачать Скриншоты Юмор Почитать Tools Проекты Обо мне Гостевая Форум |
При работе с обычными текстурами уже довольно давно используется пирамидальное фильтрование (mipmapping). Это позволяет эффективно и просто бороться с алиасингом. Основная идея в том, что вместо одной текстуры W*H текселов мы строим целую пирамиду текстур, на каждом шаге уменьшая вдвое ее размер по каждому измерению. Стандартный подход заключается в том, что на каждом уровне мы текстуру разбиваем на блоки 2х2 тексела и каждому такому блоку в следующем уровне будет соответствовать всего один тексел. Значение цвета в этом текселе будет равно среднему арифметическому цветов четырех исходный текселов.
Рис 1. Построение следующего шага пирамиды
Таким образом, при построении очередного уровня мы сглаживаем предыдущий, а сглаживание всегда означает подавление высоких частот
(детализации). Если для чтения из текстуры мы подберем соответствующий уровень, то у нас будут выполнены все условия
теоремы Котельникова, что позволяет читать из текстуры без алиасинга.
Но при этом, даже если мы читаем значения из карты глубины (например, для построения теней), то мы не используем при этом пирамидального
фильтрования. Однако текстура со значениями глубины это обычная одноканальная текстура и поэтому для нее также можно построить
всю пирамиду уровней.
Но важно понимать что строить новые значения путем усреднения значений с предыдущего уровня не имеет никакого смысла.
Есть классическая работа по иерархической видимости
(Hierarchical Z-buffer Visibility, Ned Greene, Michael Kass, Gavin Miller)
рассматривается как можно по буферу глубины построить иерархию уровней (как и в обычном пирамидальном фильтровании).
И эту иерархию можно крайне эффективно использовать для быстрого отсечения по видимости больших групп геометрии.
Современные GPU внутри себя часто используют такой иерархический буфер глубины для ускорения проверки видимости.
Однако при этом GPU использует такой иерархический буфер глубины внутри себя и все уровни кроме нулевого не доступны программисту.
В этой статье будет рассмотрено как можно по уже имеющейся текстуре со значениями глубины (depth map) можно построить полную
пирамиду и как можно использовать ее для быстрого отсечения невидимой геометрии на GPU.
Давайте рассмотрим как по i-му уровню пирамиды (размером wi*hi<) можно построить i+1-ый уровень.
Мы как и ранее будем группировать текселы в блоки 2х2 на i-ом уровне и каждому такому блоку будет соответствовать один
тексел на уровне i+1.
Если мы хотим использовать получившийся иерархический буфер глубины для быстрой проверки на видимости, то нам нужно строить
консервативную оценку видимости (т.е. когда мы можем ошибочно признать невидимый объект видимым, но никогда не признаем видимый объект
невидимым). Для получения этого свойства мы должны к значениям глубины из блока 2х2 применять операцию
Рис 2. Построение уровней при помощи
Таким образом мы при построении i+1-го уровня задаем новую область рендеринга через
Обратите внимание, что у нас размер i+1-го уровня ни по одному измерению не может быть меньше единицы.
Далее мы выводим полноэкранный прямоугольник при помощи соответствующего шейдера.
Потом эта же процедура повторяется снова и снова, пока мы не придем к текстуре размером 1х1.
Если мы имеем дело с буфером глубины, размер которого по любому измерению является степенью двух, то мы получаем простейший шейдер -
читаем четыре значения глубины, выбираем среди них максимальное и выводим его.
Однако обычно размер буфера глубины не является степенью двух, то необходимо добавить в шейдер код для обработки граничных значений.
Полученный таким образом иерархический буфер глубины можно использовать для самых разных целей, наиболее распространенными из них
являются трассировка лучей по буферу глубины (например для screen-space reflection) и быстрое отсечение заведомо невидимых
блоков геометрии.
Пусть у нас есть некоторый блок геометрии и для него у нас уже построен ограничивающий параллелепипед B.
Тогда мы можем спроектировать B на экран, построить ограничивающий проекцию AABB и подобрать для этой проекции такой уровень
иерархического буфера глубины, что вся проекция будет накрываться блоком 2х2 тексела.
Тогда мы можем довольно легко и быстро проверить видимость B относительно этого блока 2х2.
Самым простым способом сделать это будет сравнение минимального значения глубины для B и с максимальным значением глубины
в этом блоке 2х2.
Если полученное минимальное значение глубины будет больше чем максимальное значение для блока 2х2, то B гарантированно не
будет виден. А раз B не виден, то и вся геометрия, содержащаяся внутри него также будет не видна и может быть отброшена.
Рис 3. Определение невидимости AABB
В результате мы приходим к следующей схеме определения видимости.
На этапе подготовки геометрии мы разбиваем всю геометрию на группы близкорасположенных граней bi и строим ограничивающие
тела Bi для каждой группы.
Также мы выделяем (или строим) группу окклюдеров, т.е. больших граней, способных закрывать сообща много геометрии.
И этап рендеринга начинается с вывода этих окклюдеров во временный буфер глубины (только в буфер глубины).
После этого мы по полученному буферу глубины при помощи описанной ранее схемы строим иерархический буфер глубины.
Далее вычислительный шейдер для каждой группы bi проверяет соответствующее ограничивающее тело Bi на видимость относительно
построенного иерархического буфера глубины.
Если не удалось доказать невидимость Bi то вся группа bi считается видимой и в выходной буфер добавляется
запись, задающая рендеринг bi через indirect rendering.
После работы вычислительного шейдера мы получаем буфер (массив), состоящий из структур, описывающих группы, подлежащие выводу.
Для вывода потенциально-видимой геометрии мы будем использовать команду
Здесь параметр
Буфер типа
По этой
ссылке
можно скачать весь исходный код к этой статье.
max
вместо традиционного усреднения.
max
glViewport
соответствующий i+1-му уровню.
Также мы задаем что будем читать только с i-го уровня, а писать в i+1-ый.
На самом деле OpenGL позволяет читать и писать в одну и ту же текстуру, при условии что мы читаем с одного уровня, а пишем на другой.
void buildDepthPyramid ()
{
downsampleProgram.bind ();
depthMap->bind ();
// copy depth buffer contents
glCopyTexImage2D ( depthMap->getTarget (), 0, GL_DEPTH_COMPONENT, 0, 0, depthMap->getWidth (), depthMap->getHeight (), 0 );
// prepare rendering to framebuffer
fb.bind ();
// build mipmap pyramid for
// start post processing, we don't need depth test
glColorMask ( GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE );
glDepthFunc ( GL_ALWAYS );
// calculate the number of mipmap levels for NPOT texture
int w = getWidth ();
int h = getHeight ();
int numLevels = 1 + (int)floorf ( log2f ( fmaxf ( w, h ) ) );
for ( int i = 1; i < numLevels; i++ )
{
downsampleProgram.setUniformInt ( "lastMipWidth", w );
downsampleProgram.setUniformInt ( "lastMipHeight", h );
// calculate next viewport size
w = std::max ( 1, w / 2 );
h = std::max ( 1, h / 2 );
// set render area
glViewport ( 0, 0, w, h );
// bind next level for rendering but first restrict fetches only to previous level
glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, i - 1 );
glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, i - 1 );
fb.attachDepthTexture ( depthMap, i );
// draw full-screen quad
quad.render ();
}
downsampleProgram.unbind ();
// reset mipmap level range for the depth image
glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0 );
glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, numLevels - 1 );
glColorMask ( GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE );
glDepthFunc ( GL_LEQUAL );
// restore viewport
glViewport ( 0, 0, getWidth (), getHeight () );
// restore depth mip 0 as framebuffer binding
fb.attachDepthTexture ( depthMap, 0 );
fb.unbind ();
}
-- vertex
#version 460 core
layout(location = 0) in vec4 pos;
uniform vec2 scale;
uniform mat4 proj;
out vec2 tex;
out vec2 viewRay;
void main(void)
{
tex = 0.5 * ( pos.xy + vec2 ( 1.0 ) );
gl_Position = vec4 ( pos.xy, 0.0, 1.0 );
}
-- fragment
#version 460 core
uniform sampler2D lastMip;
uniform int lastMipWidth;
uniform int lastMipHeight;
in vec2 tex;
void main(void)
{
vec4 texels;
vec3 extra;
texels.x = texture ( lastMip, tex ).x;
texels.y = textureOffset ( lastMip, tex, ivec2(-1, 0) ).x;
texels.z = textureOffset ( lastMip, tex, ivec2(-1,-1) ).x;
texels.w = textureOffset ( lastMip, tex, ivec2( 0,-1) ).x;
float maxZ = max ( max ( texels.x, texels.y ), max ( texels.z, texels.w ) );
// if we are reducing an odd-width texture then the edge fragments have to fetch additional texels
if ( ( (lastMipWidth & 1) != 0 ) && ( int(gl_FragCoord.x) == lastMipWidth - 3 ) )
{
// if both edges are odd, fetch the top-left corner texel
if ( ( (lastMipHeight & 1) != 0 ) && ( int(gl_FragCoord.y) == lastMipHeight - 3 ) )
{
extra.z = textureOffset ( lastMip, tex, ivec2( 1, 1) ).x;
maxZ = max ( maxZ, extra.z );
}
extra.x = textureOffset ( lastMip, tex, ivec2( 1, 0) ).x;
extra.y = textureOffset ( lastMip, tex, ivec2( 1,-1) ).x;
maxZ = max ( maxZ, max( extra.x, extra.y ) );
}
else // if we are reducing an odd-height texture then the edge fragments have to fetch additional texels
if ( ( (lastMipHeight & 1) != 0 ) && ( int(gl_FragCoord.y) == lastMipHeight - 3 ) )
{
extra.x = textureOffset ( lastMip, tex, ivec2( 0, 1) ).x;
extra.y = textureOffset ( lastMip, tex, ivec2(-1, 1) ).x;
maxZ = max ( maxZ, max( extra.x, extra.y ) );
}
gl_FragDepth = maxZ;
}
-- compute
#version 460
layout( local_size_x = 64 ) in;
uniform mat4 mv;
uniform mat4 proj;
uniform float width;
uniform float height;
uniform vec3 extent;
uniform float zNear;
uniform float zFar;
uniform sampler2D hiZBuffer;
struct DrawElementsIndirectCommand
{
uint count;
uint instanceCount;
uint firstIndex;
uint baseVertex;
uint baseInstance;
};
layout(std430, binding = 0) buffer Pos
{
vec4 position [];
};
layout(std430, binding = 1) buffer Cmd
{
DrawElementsIndirectCommand cmds [];
};
vec4 boundingBox [8];
bool frustumCull ( in vec3 instPos, in vec3 ext )
{
mat4 mvp = proj * mv;
// create the bounding box of the object
boundingBox [0] = mvp * vec4 ( instPos + vec3 ( ext.x, ext.y, ext.z ), 1.0 );
boundingBox [1] = mvp * vec4 ( instPos + vec3 ( -ext.x, ext.y, ext.z ), 1.0 );
boundingBox [2] = mvp * vec4 ( instPos + vec3 ( ext.x, -ext.y, ext.z ), 1.0 );
boundingBox [3] = mvp * vec4 ( instPos + vec3 ( -ext.x, -ext.y, ext.z ), 1.0 );
boundingBox [4] = mvp * vec4 ( instPos + vec3 ( ext.x, ext.y, -ext.z ), 1.0 );
boundingBox [5] = mvp * vec4 ( instPos + vec3 ( -ext.x, ext.y, -ext.z ), 1.0 );
boundingBox [6] = mvp * vec4 ( instPos + vec3 ( ext.x, -ext.y, -ext.z ), 1.0 );
boundingBox [7] = mvp * vec4 ( instPos + vec3 ( -ext.x, -ext.y, -ext.z ), 1.0 );
// check how the bounding box resides regarding to the view frustum
int outOfBound [6] = int[6]( 0, 0, 0, 0, 0, 0 );
// check very box vertex against each plane
for ( int i = 0; i < 8; i++ )
{
if ( boundingBox[i].x > boundingBox[i].w )
outOfBound [0]++;
if ( boundingBox [i].x < -boundingBox [i].w )
outOfBound [1]++;
if ( boundingBox [i].y > boundingBox [i].w )
outOfBound [2]++;
if ( boundingBox [i].y < -boundingBox [i].w )
outOfBound [3]++;
if ( boundingBox [i].z > boundingBox [i].w )
outOfBound [4]++;
if ( boundingBox [i].z < -boundingBox [i].w )
outOfBound [5]++;
}
for ( int i = 0; i < 6; i++ )
if ( outOfBound [i] == 8 )
return false;
return true;
}
float linearDepth ( float d )
{
return (2.0 * zNear) / (zFar + zNear - d * (zFar - zNear));
}
bool hiZHidden ( in vec3 instPos, in vec3 ext )
{
// first do frustum cull
if ( !frustumCull ( instPos, ext ) )
return true;
// perform perspective division for the bounding box
// npw boundingBox [i] in clip space [-1,1]^3
for ( int i = 0; i < 8; i++ )
boundingBox [i].xyz /= boundingBox [i].w;
vec2 boundingRect [2];
// calculate screen space bounding rectangle
// convert xy from clip space to [0,1]^2
boundingRect[0].x = min ( min ( min ( boundingBox [0].x, boundingBox [1].x ),
min ( boundingBox [2].x, boundingBox [3].x ) ),
min ( min ( boundingBox [4].x, boundingBox [5].x ),
min ( boundingBox [6].x, boundingBox [7].x ) ) ) / 2.0 + 0.5;
boundingRect[0].y = min ( min ( min ( boundingBox [0].y, boundingBox [1].y ),
min ( boundingBox [2].y, boundingBox [3].y ) ),
min ( min ( boundingBox [4].y, boundingBox [5].y ),
min ( boundingBox [6].y, boundingBox [7].y ) ) ) / 2.0 + 0.5;
boundingRect[1].x = max ( max ( max ( boundingBox [0].x, boundingBox [1].x ),
max ( boundingBox [2].x, boundingBox [3].x ) ),
max ( max ( boundingBox [4].x, boundingBox [5].x ),
max ( boundingBox [6].x, boundingBox [7].x ) ) ) / 2.0 + 0.5;
boundingRect[1].y = max ( max ( max ( boundingBox [0].y, boundingBox [1].y ),
max ( boundingBox [2].y, boundingBox [3].y ) ),
max ( max ( boundingBox [4].y, boundingBox [5].y ),
max ( boundingBox [6].y, boundingBox [7].y ) ) ) / 2.0 + 0.5;
// then depth value of the front-most (nearest) point
float instanceDepth = min ( min ( min ( boundingBox [0].z, boundingBox [1].z ),
min ( boundingBox [2].z, boundingBox [3].z ) ),
min ( min ( boundingBox [4].z, boundingBox [5].z ),
min ( boundingBox [6].z, boundingBox [7].z ) ) );
// convert min depth from [-1,1] to [0,1]
instanceDepth = instanceDepth / 2.0 + 0.5;
// calculate the bounding rectangle size in viewport coordinates
float viewSizeX = (boundingRect [1].x - boundingRect [0].x) * width;
float viewSizeY = (boundingRect [1].y - boundingRect [0].y) * height;
// calculate texture LOD used for lookup in the depth buffer texture
float LOD = ceil ( log2 ( max ( viewSizeX, viewSizeY ) / 2.0 ) );
// finally fetch the depth texture using explicit LOD lookups
vec4 samples;
samples.x = textureLod ( hiZBuffer, vec2 ( boundingRect [0].x, boundingRect [0].y ), LOD ).x;
samples.y = textureLod ( hiZBuffer, vec2 ( boundingRect [0].x, boundingRect [1].y ), LOD ).x;
samples.z = textureLod ( hiZBuffer, vec2 ( boundingRect [1].x, boundingRect [1].y ), LOD ).x;
samples.w = textureLod ( hiZBuffer, vec2 ( boundingRect [1].x, boundingRect [0].y ), LOD ).x;
// get max value depth from buffer
float maxDepth = max ( max ( samples.x, samples.y ), max ( samples.z, samples.w ) );
// object is hidden if it is farther than buffer max depth
bool isHidden = instanceDepth >= maxDepth;
return isHidden;
}
void main()
{
uint idx = gl_GlobalInvocationID.x;
if ( idx < position.length () )
{
vec3 p = position [idx].xyz;
cmds [idx].instanceCount = hiZHidden ( p, extent ) ? 0 : 1;
}
}
glMultiDrawElementsIndirect
:
void glMultiDrawElementsIndirect ( GLenum mode, GLenum type,
const void * indirect, GLsizei drawCount, GLsizei stride );
mode
задает тип выводимых примитивов (обычно это GL_TRIANGLES
),
параметр type
задает используемый тип значений в индексном буфере.
Параметр indirect
задает смещение на массив данных в буфере типа GL_DRAW_INDIRECT_BUFFER
.
Параметр drawCount
задает число элементов в этом массиве, а stride
- смещение между началами очередных элементов
(если он равен нулю, то смещение равно размеру структуры DrawElementsIndirectCommand
).
GL_DRAW_INDIRECT_BUFFER
содержим массив структур DrawElementsIndirectCommand
, где каждая такая структура задает
отдельный блок выводимой геометрии.
struct DrawElementsIndirectCommand
{
GLuint count;
GLuint instanceCount;
GLuint firstIndex;
GLint baseVertex;
GLuint baseInstance;
};
void buildVisibleList ()
{
auto sz = knot -> getBox ().getSize () * 0.5f;
if ( cull )
{
depthMap -> bind ( 0 );
depthMap -> setFilter ( GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST );
cullProgram.bind ();
cullProgram.setUniformMatrix ( "proj", camera.getProjection () );
cullProgram.setUniformMatrix ( "mv", camera.getModelView () );
cullProgram.setUniformFloat ( "width", getWidth () );
cullProgram.setUniformFloat ( "height", getHeight () );
cullProgram.setUniformVector ( "extent", sz );
cullProgram.setUniformFloat ( "zNear", getCamera ().getZNear () );
cullProgram.setUniformFloat ( "zFar", getCamera ().getZFar () );
drawBuf.bindBase ( GL_SHADER_STORAGE_BUFFER, 1 );
posBuf.bindBase ( GL_SHADER_STORAGE_BUFFER, 0 );
// run one thread of cull shader per each model
glDispatchCompute ( (numModels + 63) / 64, 1, 1 );
depthMap -> setFilter ( GL_NEAREST, GL_NEAREST );
cullProgram.unbind ();
}
}
void renderVisibles ()
{
program2.bind ();
program2.setUniformMatrix ( "proj", camera.getProjection () );
program2.setUniformMatrix ( "mv", camera.getModelView () );
knot->getVao ().bind ();
drawBuf.bind ( GL_DRAW_INDIRECT_BUFFER );
posBuf.bindBase ( GL_SHADER_STORAGE_BUFFER, 0 );
glMultiDrawElementsIndirect ( GL_TRIANGLES, GL_UNSIGNED_INT, 0, numModels, 0 );
knot->getVao ().unbind ();
program2.unbind ();
}