Иерархический буфер глубин

Иерархический буфер глубин — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020

Краткий обзор

Иерархический буфер глубин — это многоуровневый буфер глуби (Z-буфер), используемый как ускоряющая структура (acceleration structure) для запросов глубин. Как и в случае mip-цепочек текстур, размеры каждого уровня обычно являются результатами деления на степени двойки размеров буфера полного разрешения. В этой статье я расскажу о двух способах генерации иерархического буфера глубин из буфера полного разрешения.

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

Затем для случаев, в которых требуется только один уровень даунсэмплинга, я продемонстрирую, как сгенерировать этот уровень при помощи одного вызова вычислительного (compute) шейдера, использующего атомарные операции в общей памяти рабочей группы. Для моего приложения, где требуется только разрешение 1/16 x 1/16 (mip-уровень 4), способ с вычислительным шейдером в 2-3 раза быстрее, чем обычный подход с даунсэмплингом mip-цепочки в несколько проходов.

Введение

Иерархические глубины (также называемые Hi-Z) — это часто используемая в 3D-графике техника. Она используется для ускорения усечения невидимой геометрии (occlusion culling) (в CPU, а также в GPU), вычислений отражений в экранном пространстве, объёмного тумана и многого другого.

Кроме того, в GPU Hi-Z часто реализуется как часть конвейера растеризации. Быстрые операции поиска Hi-Z в кэшах на чипе позволяют целиком пропускать тайлы фрагментов, если они полностью закрыты ранее отрендеренными примитивами.

Базовая идея Hi-Z заключается в ускорении операций запросов глубин благодаря считыванию из буферов меньшего разрешения. Это быстрее, чем считывание из буфера глубин полного разрешения, по двум причинам:

  1. Один тексел (или всего несколько текселов) буфера пониженного разрешения можно использовать как приближенное значение множества текселов буфера высокого разрешения.
  2. Буфер меньшего разрешения может быть достаточно маленьким и помещаться в кэш, что сильно ускоряет выполнение операций поиска (особенно с произвольным доступом).

Содержимое даунсэмплированных уровней буфера Hi-Z зависит от способа их использования (будет ли буфер глубин «инвертирован», какие типы запросов должны использоваться). В общем случае тексел на уровне буфера Hi-Z хранит min или max всех текселов, соответствующих ему на предыдущем уровне. Иногда одновременно хранятся значения min и max. Простые усреднённые значения (которые часто используются в mip-уровнях обычных текстур) используются нечасто, потому что они редко полезны для подобных типов запросов.

Буферы Hi-Z чаще всего запрашиваются почти сразу на выходе, чтобы избежать дальнейшей обработки и выполнения более точных операций поиска в буфере полного разрешения. Например, если мы храним значения max для неинвертированного буфера глубин (в которых чем больше значение глубины, тем дальше находится объект), то можем быстро совершенно точно определить, перекрыта ли конкретная позиция в экранном пространстве буфером глубин (если её координата Z > значения (max), сохранённого в какой-то более высоком уровне (то есть с пониженным разрешением) буфера Hi-Z).

Обратите внимание на то, что я использовал фразу «совершенно точно»: если координата Z <= полученному значению (max), то неизвестно, перекрывает ли её буфер. В некоторых областях применения в случаях неопределённости может потребоваться поиск в буфере глубин полного разрешения; в других случаях этого не требуется (например, если на кону стоят только лишние вычисления, а не правильность рендеринга).

Моё приложение: рендеринг частиц в вычислительном шейдере

Я столкнулся с необходимостью использования Hi-Z при реализации рендеринга частиц в вычислительном шейдере в движке моего VR-приложения PARTICULATE. Поскольку эта техника рендеринга не использует растеризацию фиксированными функциями, ей нужно применять собственную проверку глубин для каждой частицы размером один пиксель. А так как частицы никак не сортируются, доступ к буферу глубин оказывается (в худшем из случаев) практически случайным.

Операции поиска по полноэкранной текстуре с произвольным доступом — путь к низкой производительности. Чтобы снизить нагрузку, я сначала выполняют поиск глубин в уменьшенном буфере глубин разрешением 1/16 x 1/16 от исходного. Этот буфер содержит значения глубин min, что позволяет вычислительному шейдеру рендеринга для подавляющего большинства видимых частиц пропускать тест глубин полного разрешения. (Если глубина частицы < минимальной глубины, хранящейся в буфере пониженного разрешения, то мы знаем, что она совершенно точно видна. Если она >= min, то нам нужно проверить буфер глубин полного разрешения.)

Благодаря этому тест глубин для видимых частиц в общем случае становится малозатратной операцией. (Для перекрытых геометрией частиц он более затратен, но это нас устраивает, потому что это не вызывает затрат на рендеринг, поэтому частицы по-прежнему требуют мало вычислений.)

Благодаря тому, что поиск сначала выполняется в буфере глубин пониженного разрешения (как сказано выше), время рендеринга частиц снижается максимум на 35% по сравнению со случаем, когда поиск выполняется только в буфере полного разрешения. Поэтому для моего приложения Hi-Z очень выгоден.

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

Техника 1: генерация полной Mip-цепочки

Во многих областях использования Hi-Z требуется создание полной mip-цепочки буфера глубин. Например, при выполнении occlusion culling с помощью Hi-Z ограничивающий объём проецируется в экранное пространство и спроецированный размер используется для выбора подходящего mip-уровня (чтобы в каждой проверке перекрытия участвовало фиксированное количество текселов).

Генерирование mip-цепочки из буфера глубин полного разрешения обычно является простой задачей — для каждого тексела на уровне N мы берём max (или min, или оба значения) соответствующих 4 текселов в ранее сгенерированном уровне N-1. Выполняем последовательные проходы (каждый раз вдвое уменьшая размеры), пока не получим последний mip-уровень размером 1×1.

Однако в случае буферов глубин, размеры которых не соответствуют степеням двойки, всё сложнее. Так как Hi-Z для буферов глубин часто строится из стандартных разрешений экрана (которые редко являются степенями двойки), нам нужно найти надёжное решение этой задачи.

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

Итак, что же конкретно будет означать получаемое нами значение отдельного тексела на mip-уровне N? Это должно быть минимальное значение (min) из всех текселов полноэкранного буфера глубин, занимающего то же пространство в (нормализованном) пространстве координат текстуры.

Иными словами, если отдельная координата текстуры (в интервале $[0, 1]^2$) сопоставляется (при помощи фильтрации ближайших соседей) отдельному текселу буфера полного разрешения, то этот тексел полного разрешения должен рассматриваться кандидатом на значение min, вычисляемое для тексела в каждом последующем более высоком mip-уровне, с которым сопоставляется та же координата текстуры.

Если это соответствие гарантируется, то мы будем уверены, что операция поиска на высоких mip-уровнях никогда не вернёт значение глубины > значения тексела в той же координате текстуры, соответствующей буферу полного разрешения (уровень 0). В случае отдельного N, эта гарантия сохраняется для всех уровней ниже него (< N).

Для чётных размерностей (а в случае буферов полного разрешения, являющихся степенями двойки, чётными будут размерности на каждом уровне до самого последнего, где размеры становятся равными 1) это сделать легко. В одномерном случае для тексела с индексом $i$ на уровне N нам нужно взять текселы на уровне N-1 с индексами $2$ и $2i + 1$ и найти их значение min. То есть $D_{N}[i] = text{min}(D_{N-1}[2i], D_{N-1}[2i + 1])$. Мы можем напрямую сопоставить текселы в отношении «2 к 1» (а значит, и размеры координат текстур), ведь размер каждого уровня ровно в два раза меньше предыдущего.

Иерархический буфер глубин — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020

Пример чётных размеров уровней: 6 текселов на этом уровне уменьшаются до 3 на более высоком уровне. Размеры координат текстур каждого из трёх текселов высокого уровня точно накладываются на каждые два тексела нижнего уровня. (Точки — это центры текселов, а квадраты — это размеры координаты текстуры при использовании фильтрации ближайших соседей.)

В случае нечётных размеров уровней (а у буферов полного разрешения, размер которых не является степенью двойки, будет хотя бы один уровень с нечётным размером) всё становится сложнее. Для уровня N-1 нечётного размера $mathit{dim}_{N-1}$ размер следующего уровня (N) будет равен $mathit{dim}_{N} = lfloor frac{mathit{dim}_{N-1}}{2} rfloor$, то есть $neq frac{mathit{dim}_{N-1}}{2}$.

Это означает, что теперь у нас нет чёткого сопоставления «2 к 1» текселов уровня N-1 к текселам уровня N. Теперь размер координаты текстуры каждого тексела на уровне N накладывается на размер 3 текселов на уровне N-1.

Иерархический буфер глубин — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020

Пример нечётных размеров уровня: 7 текселов этого уровня уменьшаются до 3 текселов на следующем уровне. Размеры координаты текстуры трёх текселов высокого уровня накладываются на размеры трёх текселов из нижнего уровня.

Следовательно $D_{N}[i] = text{min}(D_{N-1}[2i], D_{N-1}[2i + 1], D_{N-1}[2i + 2])$. Это означает, что один тексел на уровне N-1 иногда влияет на значение min, вычисленное для 2 текселов на уровне N. Это необходимо для сохранения описанного выше сопоставления.

Описание выше для простоты было представлено всего в одном измерении. В двух измерениях, если обе размерности уровня N-1 являются чётными, то область текселов 2×2 на уровне N-1 сопоставляется с одним текселом на уровне N. Если одна из размерностей нечётна, то область 2×3 или 3×2 на уровне N-1 сопоставляется с одним текселом на уровне N. Если нечётны обе размерности, то следует также учитывать «угловой» тексел, то есть область 3×3 на уровне N-1 сопоставляется с одним текселом на уровне N.

Пример кода

Показанный ниже код шейдера на GLSL реализует описанный нами алгоритм. Он должен выполняться для каждого последующего mip-начиная с уровня 1 (уровень 0 — это уровень полного разрешения).

uniform sampler2D u_depthBuffer;
uniform int u_previousLevel;
uniform ivec2 u_previousLevelDimensions;

void main() {
	ivec2 thisLevelTexelCoord = ivec2(gl_FragCoord);
	ivec2 previousLevelBaseTexelCoord = 2 * thisLevelTexelCoord;

	vec4 depthTexelValues;
	depthTexelValues.x = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord,
                                    u_previousLevel).r;
	depthTexelValues.y = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord + ivec2(1, 0),
                                    u_previousLevel).r;
	depthTexelValues.z = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord + ivec2(1, 1),
                                    u_previousLevel).r;
	depthTexelValues.w = texelFetch(u_depthBuffer,
                                    previousLevelBaseTexelCoord + ivec2(0, 1),
                                    u_previousLevel).r;

	float minDepth = min(min(depthTexelValues.x, depthTexelValues.y),
                         min(depthTexelValues.z, depthTexelValues.w));

    // Incorporate additional texels if the previous level's width or height (or both) 
    // are odd. 
	bool shouldIncludeExtraColumnFromPreviousLevel = ((u_previousLevelDimensions.x & 1) != 0);
	bool shouldIncludeExtraRowFromPreviousLevel = ((u_previousLevelDimensions.y & 1) != 0);
	if (shouldIncludeExtraColumnFromPreviousLevel) {
		vec2 extraColumnTexelValues;
		extraColumnTexelValues.x = texelFetch(u_depthBuffer,
                                              previousLevelBaseTexelCoord + ivec2(2, 0),
                                              u_previousLevel).r;
		extraColumnTexelValues.y = texelFetch(u_depthBuffer,
                                              previousLevelBaseTexelCoord + ivec2(2, 1),
                                              u_previousLevel).r;

		// In the case where the width and height are both odd, need to include the 
        // 'corner' value as well. 
		if (shouldIncludeExtraRowFromPreviousLevel) {
			float cornerTexelValue = texelFetch(u_depthBuffer,
                                                previousLevelBaseTexelCoord + ivec2(2, 2),
                                                u_previousLevel).r;
			minDepth = min(minDepth, cornerTexelValue);
		}
		minDepth = min(minDepth, min(extraColumnTexelValues.x, extraColumnTexelValues.y));
	}
	if (shouldIncludeExtraRowFromPreviousLevel) {
		vec2 extraRowTexelValues;
		extraRowTexelValues.x = texelFetch(u_depthBuffer,
                                           previousLevelBaseTexelCoord + ivec2(0, 2),
                                           u_previousLevel).r;
		extraRowTexelValues.y = texelFetch(u_depthBuffer,
                                           previousLevelBaseTexelCoord + ivec2(1, 2),
                                           u_previousLevel).r;
		minDepth = min(minDepth, min(extraRowTexelValues.x, extraRowTexelValues.y));
	}

	gl_FragDepth = minDepth;
}

Изъяны этого кода

Во-первых, в случае буферов глубин полного разрешения, у которых одна размерность в два с лишним раза больше другой размерности, индексы вызовов texelFetch могут выйти за пределы u_depthBuffer. (В таких случаях меньшая размерность превращается в 1 прежде, чем другая.) Я хотел использовать в этом примере texelFetch (использующий целочисленные координаты), чтобы происходящее было как можно более понятным, и не сталкивался лично с такими особо широкими/высокими буферами глубин. Если у вас возникнут такие проблемы, то можно ограничить (clamp) передаваемые texelFetch координаты или использовать texture и нормализованные координаты текстуры (в сэмплере задав ограничение по краю). При вычислении min или max стоит всегда рассматривать один тексел несколько раз на наличие пограничных случаев.

Во-вторых, несмотря на то, что первые четыре вызова texelFetch можно заменить одним textureGather, это всё усложняет (так как textureGather не может указывать mip-уровень); кроме того, я не заметил повышения скорости работы при использовании textureGather.

Производительность

Я использовал приведённый выше фрагментный шейдер для генерации двух полных mip-цепочек для двух (по одному на каждый глаз) буферов глубин в своём VR-движке. В тесте разрешение для каждого глаза составляло 1648×1776, что привело к созданию 10 дополнительных уменьшенных mip-уровней (а значит, 10 проходов). На генерацию полной цепочки для обоих глаз потребовалось 0,25 мс на NVIDIA GTX 980 и 0,30 мс на AMD R9 290.

Иерархический буфер глубин — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020

Иерархический буфер глубин — IT-МИР. ПОМОЩЬ В IT-МИРЕ 2020

Mip-уровни 4, 5 и 6, сгенерированные из буфера глубин, соответствующего показанному выше цветовому буферу. (Стоит учесть, что бОльшая часть сцены прозрачна, а значит, не влияет на буфер глубин.) Mip-уровень 4 — первый, в котором размеры (103×111) не делятся нацело на 2.

Альтернативный способ генерации mip-цепочки

Задача описанного выше алгоритма заключается в сохранении точности запросов глубины в пространстве координат текстуры (или NDC). Для полноты (и потому, что я отказался от этой гарантии в представленной ниже технике 2) я хотел бы продемонстрировать ещё один способ, который мне встречался (например, в этой статье).

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

В этом альтернативном способе при даунсэмплинге уровня с нечётной шириной (или высотой) вместо добавления для каждого выходного тексела дополнительного столбца (или строки) текселов из предыдущего (нижнего) уровня, мы выполняем эту операцию только для выходных текселов с максимальными индексами («крайних» текселов). Единственное, что это изменяет в представленном выше фрагментном шейдере — задание значений shouldIncludeExtraColumnFromPreviousLevel и shouldIncludeExtraRowFromPreviousLevel:

// If the previous level's width is odd and this is the highest-indexed "edge" texel for 
// this level, incorporate the rightmost edge texels from the previous level. The same goes 
// for the height. 
bool shouldIncludeExtraColumnFromPreviousLevel =
    (previousMipLevelBaseTexelCoords.x == u_previousLevelDimensions.x - 3);
bool shouldIncludeExtraRowFromPreviousLevel =
    (previousMipLevelBaseTexelCoords.y == u_previousLevelDimensions.y - 3);

Из-за этого крайние текселы с наибольшим индексом становятся очень «толстыми», так как каждое деление на 2 нечётной размерности приводит к тому, что они занимают пропорционально всё больший интервал нормализованного пространства координат текстуры.

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

Показанный ниже фрагмент кода выполняет перенос из пространства NDC $[-1, 1]^2$ в координаты текселов на mip-уровне higherMipLevel:

vec2 windowCoords = (0.5 * ndc.xy + vec2(0.5)) * textureSize(u_depthBuffer, 0);
// Account for texel centers being halfway between integers. 
ivec2 texelCoords = ivec2(round(windowCoords.xy - vec2(0.5)));
ivec2 higherMipLevelTexelCoords =
    min(texelCoords / (1 << higherMipLevel),
        textureSize(u_depthBuffer, higherMipLevel).xy - ivec2(1));

Техника 2: генерация одного уровня Hi-Z при помощи вычислительного шейдера

Генерация полной mip-цепочки выполняется довольно быстро, но меня немного напрягало то, что моё приложение генерирует все эти уровни, а использует только один из них (уровень 4). Кроме устранения этой небольшой неэффективности, мне также хотелось посмотреть, насколько можно всё ускорить, если использовать для генерации только нужного мне уровня один вызов вычислительного шейдера (compute shader). (Стоит заметить, что моё приложение может останавливаться на уровне 4 при использовании решения с многопроходным фрагментным шейдером, поэтому в конце этого раздела я использовал его как основу для сравнения времени выполнения.)

В большинстве случаев использования Hi-Z требуется только один уровень глубин, поэтому я считаю, что такая ситуация встречается часто. Я написал вычислительный шейдер под собственные специфические требования (генерирование уровня 4, имеющего разрешение 1/16 x 1/16 от исходного). Похожий код можно использовать для генерации разных уровней.

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

Я попробовал два основных решения на основе вычислительных шейдеров. В первом каждый поток вызывал atomicMin для одной переменной общей памяти.

Учтите, что поскольку программисты не могут (без расширений под «железо» конкретного производителя) выполнять атомарные операции над нецелочисленными значениями (а мои глубины хранятся как float), здесь необходим какой-то трюк. Поскольку неотрицательные значения с плавающей запятой стандарта IEEE 754 сохраняют свой порядок, когда их биты обрабатываются как беззнаковые целочисленные значения, мы можем использовать floatBitsToUint, чтобы привести (при помощи reinterpret cast) значения глубин float к uint, а потом вызвать atomicMin (чтобы затем выполнить uintBitsToFloat для готового минимального значения uint).

Наиболее очевидным решением с atomicMin стало бы создание 16×16 групп потоков, в которых каждый поток получает один тексел, а затем выполняет его atomicMin со значением в общей памяти. Я сравнил такой подход с использованием блоков потоков меньшего размера (8×8, 4×8, 4×4, 2×4, 2×2), в которых каждый поток получает область текселов и вычисляет собственный локальный минимум, а затем вызывает atomicMin.

Самым быстрым из всех этих протестированных решений с atomicMin и на NVIDIA, и на AMD оказалось решение с блоками потоков 4×4 (при котором каждый поток сам получает область текселов размером 4×4). Я не совсем понимаю, почему этот вариант оказался самым быстрым, но возможно, он отражает компромисс между конкуренцией атомарных операций и вычислениями в независимых потоках. Стоит также заметить, что размер рабочей группы 4×4 использует всего 16 потоков на warp/wave (а возможно использовать ещё и 32 или 64), что любопытно. В показанном ниже примере реализован этот подход.

В качестве альтернативы использованию atomicMin я попытался выполнять параллельное уменьшение при помощи техник, использованных в этой активно цитируемой презентации NVIDIA. (Основная идея заключается в использовании массива общей памяти того же размера, что и количество потоков в рабочей группе, а также $log_{2} (n)$ проходов для последовательного совместного вычисления min минимумов каждого потока, пока не будет получен окончательный минимум всей рабочей группы.)

Я попробовал это решение со всеми теми же размерами рабочих групп, что и в решении с atomicMin. Даже со всеми оптимизациями, описанными в презентации NVIDIA, решение с параллельным уменьшением слегка медленнее (на обоих GPU в пределах десятка процентов), чем решение с atomicMin, к которому пришёл я. Кроме того, решение с atomicMin значительно проще с точки зрения кода.

Пример кода

При таком способе проще всего не стараться поддерживать соответствие в нормализованном пространстве координат текстуры между текселами уменьшенных буферов и полного разрешения. Можно просто выполнять преобразования из координат текселов полного разрешения в координаты текселов уменьшенного разрешения:

ivec2 reducedResTexelCoords = texelCoords / ivec2(downscalingFactor);

В моём случае (генерирование эквивалента mip-уровня 4) downscalingFactor равен 16.

Как сказано выше, этот вычислительный шейдер GLSL реализует решение с atomicMin с размером рабочих групп 4×4, где каждый поток получает из буфера полного разрешения область текселов размером 4×4. Получившийся уменьшенный буфер глубин значений min равен 1/16 x 1/16 от размера буфера полного разрешения (с округлением вверх, когда размеры полного разрешения не делятся на 16 нацело).

uniform sampler2D u_inputDepthBuffer;
uniform restrict writeonly image2DArray u_outputDownsampledMinDepthBufferImage;
// The dimension in normalized texture coordinate space of a single texel in 
// u_inputDepthBuffer. 
uniform vec2 u_texelDimensions;

// Resulting image is 1/16th x 1/16th resolution, but we fetch 4x4 texels per thread, hence 
// the divisions by 4 here. 
layout(local_size_x = 16/4, local_size_y = 16/4, local_size_z = 1) in;

// This is stored as uint because atomicMin only works on integer types. Luckily 
// (non-negative) floats maintain their order when their bits are interpreted as uint (using 
// floatBitsToUint). 
shared uint s_workgroupMinDepthEncodedAsUint;

void main() {
	if (gl_LocalInvocationIndex == 0) {
        // Initialize to 1.0 (max depth) before performing atomicMin's. 
		s_workgroupMinDepthEncodedAsUint = floatBitsToUint(1.0);
	}

	memoryBarrierShared();
	barrier();

	// Fetch a 4x4 texel region per thread with 4 calls to textureGather. 'gatherCoords' 
    // are set up to be equidistant from the centers of the 4 texels being gathered (which 
    // puts them on integer values). In my tests textureGather was not faster than 
    // individually fetching each texel - I use it here only for conciseness. 
    // 
    // Note that in the case of the full-res depth buffer's dimensions not being evenly 
    // divisible by the downscaling factor (16), these textureGather's may try to fetch 
    // out-of-bounds coordinates - that's fine as long as the texture sampler is set to 
    // clamp-to-edge, as redundant values don't affect the resulting min. 

	uvec2 baseTexelCoords = 4 * gl_GlobalInvocationID.xy;
	vec2 gatherCoords1 = (baseTexelCoords + uvec2(1, 1)) * u_texelDimensions;
	vec2 gatherCoords2 = (baseTexelCoords + uvec2(3, 1)) * u_texelDimensions;
	vec2 gatherCoords3 = (baseTexelCoords + uvec2(1, 3)) * u_texelDimensions;
	vec2 gatherCoords4 = (baseTexelCoords + uvec2(3, 3)) * u_texelDimensions;

	vec4 gatheredTexelValues1 = textureGather(u_inputDepthBuffer, gatherCoords1);
	vec4 gatheredTexelValues2 = textureGather(u_inputDepthBuffer, gatherCoords2);
	vec4 gatheredTexelValues3 = textureGather(u_inputDepthBuffer, gatherCoords3);
	vec4 gatheredTexelValues4 = textureGather(u_inputDepthBuffer, gatherCoords4);

	// Now find the min across the 4x4 region fetched, and apply that to the workgroup min 
    // using atomicMin. 
	vec4 gatheredTexelMins = min(min(gatheredTexelValues1, gatheredTexelValues2),
                                 min(gatheredTexelValues3, gatheredTexelValues4));
	float finalMin = min(min(gatheredTexelMins.x, gatheredTexelMins.y),
                         min(gatheredTexelMins.z, gatheredTexelMins.w));
	atomicMin(s_workgroupMinDepthEncodedAsUint, floatBitsToUint(finalMin));

	memoryBarrierShared();
	barrier();

    // Thread 0 writes workgroup-wide min to image. 
	if (gl_LocalInvocationIndex == 0) {
		float workgroupMinDepth = uintBitsToFloat(s_workgroupMinDepthEncodedAsUint);

		imageStore(u_outputDownsampledMinDepthBufferImage,
		           ivec2(gl_WorkGroupID.xy),
                   // imageStore can only be passed vec4, but only a float is stored. 
				   vec4(workgroupMinDepth));
	}
}

Производительность

Я использовал приведённый выше вычислительный шейдер для обработки буфера глубин полного разрешения с теми же размерами, которые использовались для генерации полной mip-цепочки (буферы 1648×1776 для каждого глаза). Он выполняется за 0,12 мс на NVIDIA GTX 980 и за 0,08 мс на AMD R9 290. Если сравнивать со временем генерации только mip-уровней 1–4 (0,22 мс на NVIDIA, 0,25 мс AMD), то решение с вычислительным шейдером оказалось на 87% быстрее у GPU NVIDIA и на 197% быстрее, чем у GPU AMD.

В абсолютных величинах ускорение не такое уж и большое, но важны каждые 0,1 мс, особенно в VR 🙂

Специально для сайта ITWORLD.UZ. Новость взята с сайта Хабр