Buffer de profundidad jerárquica


Breve reseña


Un búfer de profundidad jerárquico es un búfer de profundidad multinivel (Z-buffer) utilizado como estructura de aceleración para consultas de profundidad. Como en el caso de las cadenas de mip de textura, los tamaños de cada nivel son generalmente el resultado de dividir por el grado de dos los tamaños del búfer de resolución completa. En este artículo, hablaré sobre dos formas de generar un búfer de profundidad jerárquica a partir de un búfer de resolución completa.

Primero, le mostraré cómo generar una cadena de mip completa para un búfer de profundidad, que conserva la precisión de las consultas de profundidad en el espacio de coordenadas de textura (o NDC) incluso para tamaños de búfer de profundidad que no son iguales a las potencias de dos. (En Internet, he encontrado ejemplos de código que no garantizan esta precisión, lo que complica la ejecución de consultas precisas a niveles altos de mip).

Luego, para los casos en que solo se requiere un nivel de disminución de resolución, demostraré cómo generar este nivel con una sola llamada a un sombreador de cómputo que utiliza operaciones atómicas en la memoria compartida del grupo de trabajo. Para mi aplicación, donde solo se requiere una resolución de 1/16 x 1/16 (nivel 4 de mip), el método con un sombreador computacional es 2-3 veces más rápido que el enfoque habitual con la disminución de la cadena de mip en varias pasadas.

Introducción


Las profundidades jerárquicas (también llamadas Hi-Z) es una técnica que se usa a menudo en gráficos 3D. Se utiliza para acelerar el recorte de la geometría invisible (eliminación de oclusión) (en la CPU , así como en la GPU ), el cálculo de los reflejos en el espacio de la pantalla , la niebla volumétrica y mucho más.

Además, las GPU Hi-Z a menudo se implementan como parte de una canalización de rasterización. Las operaciones de búsqueda rápida Hi-Z en los cachés del chip le permiten omitir por completo los mosaicos de fragmentos si están completamente cubiertos por primitivas renderizadas previamente.

La idea básica de Hi-Z es acelerar las operaciones de consulta en profundidad leyendo desde memorias intermedias de menor resolución. Esto es más rápido que leer profundidades de resolución completa desde el búfer, por dos razones:

  1. Un texel (o solo unos pocos texels) de un buffer de baja resolución puede usarse como un valor aproximado de una pluralidad de texels de un buffer de alta resolución.
  2. Un búfer de menor resolución puede ser lo suficientemente pequeño y almacenado en caché, lo que acelera enormemente la ejecución de las operaciones de búsqueda (especialmente con acceso aleatorio).

El contenido de los niveles de búfer Hi-Z muestreados hacia abajo depende de cómo se usen (si el búfer de profundidad se "invertirá" , qué tipos de solicitudes se deben usar). En general, un texel en el nivel de búfer Hi-Z almacena mino maxtodos los texel correspondientes en el nivel anterior. A veces los valores miny se almacenan al mismo tiempo max. Los valores promediados simples (que a menudo se usan en los niveles mip de las texturas regulares) se usan con poca frecuencia porque rara vez son útiles para este tipo de consultas.

Los buffers Hi-Z se solicitan con mayor frecuencia casi inmediatamente a la salida para evitar un mayor procesamiento y operaciones de búsqueda más precisas en el buffer de resolución completa. Por ejemplo, si almacenamos valoresmaxpara un búfer de profundidad no invertido (en el que cuanto mayor es el valor de profundidad, más lejos está el objeto), podemos determinar rápidamente exactamente si una posición particular en el espacio de la pantalla está cubierta por un búfer de profundidad (si su coordenada Z> es el valor (máximo) almacenado en algunos nivel más alto (es decir, resolución más baja) del búfer Hi-Z).

Tenga en cuenta que usé la frase "exactamente": si la coordenada Z <= el valor recibido (máx.), Entonces no se sabe si su búfer se superpone. En algunas aplicaciones, en casos de incertidumbre, puede ser necesario buscar en el búfer profundidades de resolución completa; en otros casos, esto no es obligatorio (por ejemplo, si solo están en juego cálculos innecesarios y no la representación correcta).

Mi aplicación: renderizar partículas en un sombreador computacional


Me enfrenté a la necesidad de usar Hi-Z al implementar el renderizado de partículas en un sombreador computacional en el motor de mi aplicación PARTICULATE VR . Dado que esta técnica de renderizado no usa rasterización con funciones fijas, necesita usar su propia verificación de profundidad para cada partícula con un tamaño de un píxel. Y dado que las partículas no están clasificadas de ninguna manera, el acceso al búfer de profundidad es (en el peor de los casos) casi aleatorio.

Las operaciones de búsqueda en una textura de acceso aleatorio a pantalla completa son el camino hacia un bajo rendimiento. Para reducir la carga, primero busco profundidades en el búfer de profundidad reducida con una resolución de 1/16 x 1/16 del original. Este búfer contiene valores de profundidad.min, que permite que el sombreador de renderizado computacional para la gran mayoría de las partículas visibles omita la prueba de profundidad de resolución completa. (Si la profundidad de la partícula es <la profundidad mínima almacenada en el búfer de resolución más baja, entonces sabemos que es absolutamente visible. Si es> = min, entonces debemos verificar el búfer de profundidad de resolución completa).

Gracias a esto, la prueba de profundidad para partículas visibles en el caso general se convierte en una operación de bajo costo. (Para las partículas superpuestas por la geometría, es más costoso, pero nos conviene porque no causa costos de procesamiento, por lo tanto, las partículas aún requieren poco cálculo).

Debido a que la búsqueda se realiza primero en el búfer de profundidades de menor resolución (como se mencionó anteriormente) , el tiempo de reproducción de partículas se reduce en un máximo del 35%en comparación con el caso cuando la búsqueda se realiza solo en el búfer de resolución completa. Por lo tanto, para mi aplicación, Hi-Z es muy beneficioso.

Ahora veremos dos técnicas para generar un búfer de profundidad jerárquico.

Técnica 1: generar una cadena Mip completa


En muchas aplicaciones Hi-Z, se requiere la creación de una cadena de mip de memoria intermedia de profundidad completa. Por ejemplo, cuando se realiza la eliminación de oclusiones usando Hi-Z, el volumen delimitador se proyecta en el espacio de la pantalla y el tamaño proyectado se usa para seleccionar el nivel de mip apropiado (de modo que se involucra un número fijo de texels en cada prueba de superposición).

Generar una cadena mip a partir del búfer de profundidad de resolución completa suele ser una tarea simple: para cada texel en el nivel N tomamos max( mino ambos) los 4 texels correspondientes en el nivel N-1 generado anteriormente. Realizamos pases secuenciales (cada vez reduciendo el tamaño a la mitad) hasta que obtengamos el último nivel de mip 1x1 en tamaño.

Sin embargo, en el caso de los amortiguadores de profundidad, cuyos tamaños no corresponden a las potencias de dos, todo es más complicado. Dado que Hi-Z para buffers de profundidad a menudo se construye a partir de resoluciones de pantalla estándar (que rara vez son potencias de dos), necesitamos encontrar una solución confiable para este problema.

Primero decidamos qué significará el valor de cada búfer de profundidad de nivel mip de texel. En este artículo, asumiremos que la cadena mip almacena valores min. Las operaciones de búsqueda de profundidad deberían utilizar el filtrado de los vecinos más cercanos, porque la interpolación de valores es mininútil para nosotros y dañará la naturaleza jerárquica de la cadena de profundidades creada.

Entonces, ¿qué significa exactamente el valor del texel individual en el nivel mip N obtenido por nosotros? Este debería ser el valor mínimo (min) de todos los texels del búfer de profundidad de pantalla completa que ocupa el mismo espacio en el espacio de coordenadas de textura (normalizado).

En otras palabras, si una coordenada separada de la textura (en el intervalo ) se asigna (filtrando los vecinos más cercanos) a un texel individual del búfer de resolución completa, luego este texel de resolución completa debe considerarse como un candidato para el valorcalculado para el texel en cada nivel de mip superior posterior con el que se asigna la misma coordenada texturas Si se garantiza esta correspondencia, nos aseguraremos de que la operación de búsqueda en niveles altos de mip nunca devolverá el valor de profundidad> valor de texel en la misma coordenada de textura correspondiente al búfer de resolución completa (nivel 0). En el caso de una N separada, esta garantía se mantiene para todos los niveles por debajo de ella (<N).[0,1]2min



Para las dimensiones pares (y en el caso de las memorias intermedias de resolución completa, que son potencias de dos, las dimensiones pares en cada nivel hasta el último, donde las dimensiones se vuelven iguales a 1) serán fáciles de hacer. En el caso unidimensional, para texel con un índicei en el nivel N necesitamos tomar texels en el nivel N-1 con índices2 y2i+1 y encuentra su valormin. Es decirDN[i]=min(DN1[2i],DN1[2i+1]) . Podemos comparar directamente los texels en la relación "2 a 1" (y, por lo tanto, el tamaño de las coordenadas de textura), porque el tamaño de cada nivel es exactamente dos veces menor que el anterior.


Un ejemplo de tamaños de nivel pares: 6 texels en este nivel se reducen a 3 en un nivel superior. Los tamaños de coordenadas de textura de cada uno de los tres texels de alto nivel se superponen con precisión en cada dos texels de nivel inferior. (Los puntos son los centros de los texels, y los cuadrados son las dimensiones de la coordenada de textura cuando se filtran los vecinos más cercanos).

En el caso de tamaños de nivel impares (y los buffers de resolución completa que no son una potencia de dos tendrán al menos un nivel con un tamaño impar) todo cada vez más difícil Para el nivel N-1 de tamaño impar tamaño del siguiente nivel (N) será igualdimN1, es decirdimN=dimN12 . Esto significa que ahora no tenemos un mapeo claro 2 a 1 de los texels del nivel N-1 a los texels del nivel N.Ahora el tamaño de la coordenada de textura de cada texel en el nivel N se superpone al tamaño de3texels en el nivel N-1.dimN12




Un ejemplo de un tamaño de nivel impar: 7 texels de este nivel se reducen a 3 texels en el siguiente nivel. Las dimensiones de las coordenadas de textura de los tres texels de alto nivel se superponen a los tamaños de los tres texels del nivel inferior.

Por lo tantoDN[i]=min(DN1[2i],DN1[2i+1],DN1[2i+2]) . Esto significa que un texel al nivel de N-1 a veces afecta el valormincalculado para2texel al nivel de N. Esto es necesario para mantener la comparación descrita anteriormente.

La descripción anterior se ha presentado en una sola dimensión por simplicidad. En dos dimensiones, si ambas dimensiones del nivel N-1 son pares, entonces la región 2x2 texel en el nivel N-1 se asigna a un texel en el nivel N. Si una de las dimensiones es impar, la región 2x3 o 3x2 en el nivel N-1 se asigna a uno texel en el nivel N. Si ambas dimensiones son impares , entonces el texel "angular" también debe tenerse en cuenta, es decir, la región 3x3 en el nivel N-1 se compara con un texel en el nivel N.

Ejemplo de código


El código de sombreador GLSL que se muestra a continuación implementa el algoritmo que describimos. Debe ejecutarse para cada mip posterior, comenzando desde el nivel 1 (el nivel 0 es el nivel de resolución completa).

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;
}

Defectos en este código


Primero, en el caso de memorias intermedias de profundidad de resolución completa para las cuales una dimensión es más de dos veces el tamaño de otra dimensión, los índices de llamadas texelFetchpueden ir más allá u_depthBuffer. (En tales casos, la dimensión más pequeña se convierte en 1 antes que la otra). Quería usar en este ejemplo texelFetch(usando coordenadas enteras), de modo que lo que estaba sucediendo fuera lo más claro posible, y no encontré personalmente tales amortiguadores particularmente anchos / profundos. Si encuentra tales problemas, puede limitar ( clamp) las texelFetchcoordenadas transmitidas o usar las texturecoordenadas normalizadas de la textura (en la muestra, establezca un límite en el borde). Al calcular mino maxsiempre debe considerar un mensaje de texto varias veces por la presencia de casos límite.

En segundo lugar, a pesar de que las primeras cuatro llamadas texelFetchse pueden reemplazar por una textureGather, esto complica las cosas (ya que textureGatherno se puede indicar el nivel de mip); Además, no noté un aumento en la velocidad al usarlo textureGather.

Actuación


Utilicé el sombreador de fragmentos anterior para generar dos cadenas mip completas para dos amortiguadores de profundidad (uno para cada ojo) en mi motor VR. En la prueba, la resolución para cada ojo fue de 1648x1776, lo que condujo a la creación de 10 niveles de mip reducidos adicionales (lo que significa 10 pases). Tomó 0.25 ms en el NVIDIA GTX 980 y 0.30 ms en el AMD R9 290 para generar una cadena completa para ambos ojos.



Mip- 4, 5 6, , . ( , , , .) Mip- 4 — , (103x111) 2.

mip-


La tarea del algoritmo descrito anteriormente es mantener la precisión de las consultas de profundidad en el espacio de coordenadas de textura (o NDC). Para completar (y porque rechacé esta garantía en la técnica a continuación 2), me gustaría demostrar un método más que encontré (por ejemplo, en este artículo ).

Tenga en cuenta que, como el anterior, este método alternativo está diseñado para funcionar con memorias intermedias de resolución completa cuyos tamaños no son potencias de dos (pero, por supuesto, funcionan con tamaños iguales a potencias de dos).

En este método alternativo, cuando se muestrea un nivel con un ancho (o alto) impar en lugar de agregar una columna (o fila) adicional de elementos de textura desde el nivel anterior (inferior) para cada elemento de salida, realizamos esta operación solo para elementos de salida con índices máximos (elementos "extremos" ) Lo único que cambia en el sombreador de fragmentos presentado anteriormente es establecer valores shouldIncludeExtraColumnFromPreviousLevely 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);

Debido a esto, los texels extremos con el índice más alto se vuelven muy "gruesos", ya que cada división por 2 dimensiones impares lleva al hecho de que ocupan un intervalo proporcionalmente mayor del espacio de coordenadas de textura normalizado.

La desventaja de este enfoque es que se vuelve más difícil realizar consultas de profundidad de niveles altos de mip. En lugar de simplemente usar las coordenadas de textura normalizadas, primero debemos determinar el texel de resolución completa correspondiente a estas coordenadas, y luego transferir las coordenadas de este texel a las coordenadas del nivel de mip correspondiente, cuya solicitud se está ejecutando.

El fragmento de código a continuación se está migrando desde el espacio NDC[1,1]2 a las coordenadas texel en el nivel miphigherMipLevel:

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));

Técnica 2: generar un único nivel Hi-Z usando un sombreador de computación


Generar una cadena de mip completa es bastante rápido, pero me molestó un poco que mi aplicación genere todos estos niveles y use solo uno de ellos (nivel 4). Además de eliminar esta leve ineficiencia, también quería ver cuánto se podía acelerar todo si usaba solo una llamada de sombreador de cómputo para generar el nivel que necesitaba. (Vale la pena señalar que mi aplicación puede detenerse en el nivel 4 cuando uso una solución con un sombreador de fragmentos de múltiples pasadas, por lo que al final de esta sección la utilicé como base para comparar tiempos de ejecución).

En la mayoría de las aplicaciones Hi-Z, solo se requiere un nivel de profundidad, por lo que creo que esta situación es común. Escribí un sombreador computacional para mis propios requisitos específicos (generando el nivel 4, que tiene una resolución de 1/16 x 1/16 del original). Se puede usar un código similar para generar diferentes niveles.

El sombreador computacional es muy adecuado para esta tarea, ya que puede usar memoria compartida de grupo de trabajo para intercambiar datos entre subprocesos. Cada grupo de trabajo es responsable de un texel de salida (reducido por disminución de muestreo del búfer), y los hilos del grupo de trabajo comparten el trabajo de calcular los mintexels correspondientes de resolución completa, compartiendo los resultados a través de la memoria compartida.

Probé dos soluciones principales basadas en sombreadores computacionales. En el primero, cada hilo requería atomicMinuna variable de memoria compartida.

Tenga en cuenta que dado que los programadores no pueden (sin extensiones para el hardware de un fabricante en particular) realizar operaciones atómicas en valores no enteros (y mis profundidades se almacenan como float), aquí se necesita algún truco. Dado que los valores de coma flotante no negativos de la norma IEEE 754 mantienen su orden cuando sus bits se procesan como valores enteros sin signo, podemos utilizar floatBitsToUintpara llevar (usando fundido reinterpretar) los valores de profundidad floata uint, y luego llamar atomicMin(a continuación, ejecutar uintBitsToFloatpara el valor mínimo acabado uint) .

La solución más obvia atomicMinsería crear grupos de subprocesos de 16x16 en los que cada subproceso reciba un texel y luego lo ejecute atomicMincon un valor en la memoria compartida. Comparé este enfoque utilizando bloques de flujo más pequeños (8x8, 4x8, 4x4, 2x4, 2x2), en los que cada flujo recibe una región de texel y calcula su propio mínimo local, y luego llama atomicMin.

La más rápida de todas estas soluciones probadas conatomicMinNVIDIA y AMD resultaron tener una solución con bloques de flujo 4x4 (en el que cada flujo recibe un área de texel 4x4). No entiendo bien por qué esta opción resultó ser la más rápida, pero tal vez refleja un compromiso entre la competencia de las operaciones atómicas y los cálculos en flujos independientes. También vale la pena señalar que el tamaño del grupo de trabajo 4x4 usa solo 16 hilos por urdimbre / onda (y es posible usar también 32 o 64), lo cual es interesante. El siguiente ejemplo implementa este enfoque.

Como alternativa al uso, atomicMinintenté realizar una reducción paralela utilizando las técnicas utilizadas en esta presentación de NVIDIA citada activamente. (La idea básica es utilizar una matriz de memoria compartida del mismo tamaño que el número de subprocesos en el grupo de trabajo, así como pasa para el cálculo conjunto secuencial de losmínimos de cada subproceso hasta que se obtiene el mínimo final de todo el grupo de trabajo.) Probé esta solución con los mismos tamaños de grupo de trabajo que en la solución c. Incluso con todas las optimizaciones descritas en la presentación de NVIDIA, la solución de reducción paralela es ligeramente más lenta (dentro de diez GPU en ambas GPU) que la solución a laque llegué. Además, la solución con esmucho más simple en términos de código.log2(n)min

atomicMinatomicMinatomicMin

Ejemplo de código


Con este método, la forma más fácil es no tratar de mantener la correspondencia en el espacio normalizado de coordenadas de textura entre texels de amortiguadores reducidos y resolución completa. Simplemente puede realizar conversiones desde coordenadas texel de resolución completa a coordenadas texel de resolución reducida:

ivec2 reducedResTexelCoords = texelCoords / ivec2(downscalingFactor);

En mi caso (generar el equivalente de mip-level 4) downscalingFactores 16.

Como se mencionó anteriormente, este sombreador computacional GLSL implementa una solución con atomicMintamaños de grupo de trabajo 4x4, donde cada hilo recibe un área de texel 4x4 del búfer de resolución completa. El búfer de profundidad de valor reducido resultante mines 1/16 x 1/16 del tamaño del búfer de resolución completa (redondeado cuando los tamaños de resolución completa no son divisibles por 16 por completo).

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));
	}
}

Actuación


Utilicé el sombreador computacional anterior para procesar el búfer de profundidad de resolución completa con las mismas dimensiones que se usaron para generar la cadena de mip completa (búferes de 1648x1776 para cada ojo). Se ejecuta en 0.12 ms en el NVIDIA GTX 980 y 0.08 ms en el AMD R9 290. Si lo comparamos con el tiempo de generación de solo niveles mip 1-4 (0.22 ms en NVIDIA, 0.25 ms AMD), entonces La solución con un sombreador computacional resultó ser 87% más rápida con las GPU NVIDIA y 197% más rápida que las GPU AMD .

En términos absolutos, la aceleración no es tan grande, pero cada 0.1 ms es importante, especialmente en VR :)

All Articles