Buffer de profundidade hierárquica


Breve revisão


Um buffer de profundidade hierárquico é um buffer de profundidade de vários níveis (Z-buffer) usado como uma estrutura de aceleração para consultas de profundidade. Como no caso de cadeias mip de textura, os tamanhos de cada nível geralmente são o resultado da divisão pelo grau de dois dos tamanhos do buffer de resolução total. Neste artigo, falarei sobre duas maneiras de gerar um buffer de profundidade hierárquico a partir de um buffer de resolução completa.

Primeiro, mostrarei como gerar uma cadeia mip completa para um buffer de profundidade, que preserva a precisão das consultas de profundidade no espaço de coordenadas da textura (ou NDC), mesmo para tamanhos de buffer de profundidade diferentes de potências de dois. (Na Internet, deparei-me com exemplos de código que não garantem essa precisão, o que complica a execução de consultas precisas em altos níveis de mip.)

Então, nos casos em que apenas um nível de downsampling é necessário, demonstrarei como gerar esse nível com uma única chamada para um shader de computação que usa operações atômicas na memória compartilhada do grupo de trabalho. Para minha aplicação, em que apenas é necessária a resolução 1/16 x 1/16 (nível mip 4), o método com um shader computacional é 2-3 vezes mais rápido do que a abordagem usual com a redução da amostragem da cadeia mip em várias passagens.

Introdução


Profundidades hierárquicas (também chamadas de Hi-Z) é uma técnica frequentemente usada em gráficos 3D. É usado para acelerar o corte da geometria invisível (seleção de oclusão) (na CPU , bem como na GPU ), o cálculo de reflexões no espaço da tela , neblina volumétrica e muito mais.

Além disso, as GPUs Hi-Z são frequentemente implementadas como parte de um pipeline de rasterização. As operações de pesquisa rápida Hi-Z nos caches do chip permitem que você pule completamente os blocos de fragmentos se eles estiverem completamente cobertos por primitivas renderizadas anteriormente.

A idéia básica do Hi-Z é acelerar as operações de consulta em profundidade lendo os buffers de baixa resolução. Isso é mais rápido do que ler profundidades de resolução total do buffer, por dois motivos:

  1. Um texel (ou apenas alguns texels) de um buffer de resolução mais baixa pode ser usado como um valor aproximado de uma pluralidade de texels de um buffer de alta resolução.
  2. Um buffer de resolução mais baixa pode ser pequeno o suficiente e armazenado em cache, o que acelera bastante a execução de operações de pesquisa (especialmente com acesso aleatório).

O conteúdo dos níveis de buffer Hi-Z com amostragem reduzida depende de como eles são usados ​​(se o buffer de profundidade será "invertido" , que tipos de solicitações devem ser utilizados). Em geral, um texel no nível do buffer Hi-Z armazena minou maxtodos os texels correspondentes a ele no nível anterior. Às vezes, os valores de mine são armazenados ao mesmo tempo max. Valores médios simples (geralmente usados ​​nos níveis mip de texturas regulares) são usados ​​com pouca frequência porque raramente são úteis para esses tipos de consultas.

Os buffers Hi-Z são frequentemente solicitados quase imediatamente na saída para evitar processamento adicional e operações de pesquisa mais precisas no buffer de resolução completa. Por exemplo, se armazenarmos valoresmaxpara um buffer de profundidade não invertido (no qual quanto maior o valor de profundidade, maior o objeto), podemos determinar rapidamente exatamente se uma posição específica no espaço da tela é coberta por um buffer de profundidade (se sua coordenada Z> for o valor (máximo) armazenado em alguma nível mais alto (ou seja, resolução mais baixa) do buffer Hi-Z).

Observe que eu usei a frase “exatamente”: se a coordenada Z <= o valor recebido (máx), não se sabe se o buffer se sobrepõe. Em algumas aplicações, em casos de incerteza, pode ser necessário procurar no buffer a profundidade da resolução total; em outros casos, isso não é necessário (por exemplo, se apenas cálculos desnecessários estiverem em jogo e não a renderização correta).

Minha aplicação: renderizando partículas em um shader computacional


Fui confrontado com a necessidade de usar o Hi-Z ao implementar a renderização de partículas em um shader computacional no mecanismo do meu aplicativo PARTICULATE VR . Como essa técnica de renderização não usa rasterização com funções fixas, ela precisa usar sua própria verificação de profundidade para cada partícula com o tamanho de um pixel. E como as partículas não são classificadas de maneira alguma, o acesso ao buffer de profundidade é (no pior dos casos) quase aleatório.

As operações de pesquisa em uma textura de acesso aleatório em tela cheia são o caminho para um desempenho ruim. Para reduzir a carga, primeiro procuro profundidades no buffer de profundidade reduzida com uma resolução de 1/16 x 1/16 do original. Este buffer contém valores de profundidade.min, que permite que o shader de renderização computacional para a grande maioria das partículas visíveis pule o teste de profundidade de resolução total. (Se a profundidade da partícula é <a profundidade mínima armazenada no buffer de resolução mais baixa, sabemos que é absolutamente visível. Se> = min, precisamos verificar o buffer de profundidade da resolução total.)

Graças a isso, o teste de profundidade para partículas visíveis no caso geral torna-se uma operação de baixo custo. (Para partículas sobrepostas pela geometria, é mais caro, mas nos convém porque não causa custos de renderização, portanto as partículas ainda exigem pouco cálculo.)

Devido ao fato de a pesquisa ser realizada pela primeira vez no buffer de profundidades de menor resolução (como mencionado acima) , o tempo de renderização das partículas é reduzido em 35% no máximocomparado ao caso em que a pesquisa é realizada apenas no buffer de resolução completa. Portanto, para o meu aplicativo, o Hi-Z é muito benéfico.

Agora, examinaremos duas técnicas para gerar um buffer de profundidade hierárquico.

Técnica 1: gerando uma cadeia Mip completa


Em muitas aplicações Hi-Z, é necessária a criação de uma cadeia mip de buffer de profundidade completa. Por exemplo, ao executar o descarte de oclusão usando o Hi-Z, o volume delimitador é projetado no espaço da tela e o tamanho projetado é usado para selecionar o nível mip apropriado (para que um número fixo de texels esteja envolvido em cada teste de sobreposição).

Gerar uma cadeia mip a partir do buffer de profundidade de resolução total é geralmente uma tarefa simples - para cada texel no nível N, pegamos max(ou minou ambos) os 4 texels correspondentes no nível N-1 gerado anteriormente. Executamos passes sequenciais (cada vez que reduzimos o tamanho pela metade) até obtermos o último nível mip 1x1 de tamanho.

No entanto, no caso de buffers de profundidade, cujos tamanhos não correspondem às potências de dois, tudo é mais complicado. Como o Hi-Z para buffers de profundidade geralmente é criado a partir de resoluções de tela padrão (que raramente são potências de dois), precisamos encontrar uma solução confiável para esse problema.

Vamos primeiro decidir o que significa o valor de cada buffer de profundidade no nível mip texel. Neste artigo, assumiremos que a cadeia mip armazena valores min. As operações de pesquisa de profundidade devem usar a filtragem dos vizinhos mais próximos, porque a interpolação de valores é mininútil para nós e prejudicará a natureza hierárquica da cadeia de profundidades criada.

Então, o que exatamente o valor do texel individual no nível mip N obtido por nós significa? Esse deve ser o valor mínimo (min) de todos os texels do buffer de profundidade de tela inteira que ocupa o mesmo espaço no espaço de coordenadas da textura (normalizada).

Em outras palavras, se uma coordenada separada da textura (no intervalo[0,1]2) é mapeado (filtrando os vizinhos mais próximos) para um texel individual do buffer de resolução completa, esse texel de resolução total deve ser considerado um candidato para o valor mincalculado para o texel em cada nível mip mais alto subsequente, com o qual a mesma coordenada de textura é mapeada.

Se essa correspondência for garantida, teremos certeza de que a operação de busca em altos níveis de mip nunca retornará o valor de profundidade> valor texel na mesma coordenada de textura correspondente ao buffer de resolução total (nível 0). No caso de um N separado, essa garantia é mantida para todos os níveis abaixo dele (<N).

Para dimensões pares (e no caso de buffers de resolução total, que são potências de dois, dimensões pares em cada nível até o último, onde as dimensões se tornam iguais a 1), será fácil. No caso unidimensional, para texel com um índicei no nível N, precisamos pegar texels no nível N-1 com índices 2e 2i+1e encontre seu significado min. I.eDN[i]=min(DN1[2i],DN1[2i+1]). Podemos comparar diretamente os texels na proporção “2 para 1” (e, portanto, o tamanho das coordenadas da textura), porque o tamanho de cada nível é exatamente duas vezes menor que o anterior.


Um exemplo de tamanhos de níveis pares: 6 texels nesse nível são reduzidos para 3 em um nível superior. Os tamanhos das coordenadas de textura de cada um dos três texels de alto nível são sobrepostos com precisão em cada dois texels de nível inferior. (Pontos são os centros de texels e quadrados são as dimensões da coordenada da textura ao filtrar os vizinhos mais próximos.)

No caso de tamanhos de níveis ímpares (e buffers de resolução total que não são uma potência de dois terão pelo menos um nível com um tamanho ímpar) tudo ficando mais difícil. Para o nível N-1 de tamanho ímpardimN1 o tamanho do próximo nível (N) será igual dimN=dimN12, isto é dimN12.

Isso significa que agora não temos um mapeamento 2 a 1 claro de texels do nível N-1 para texels do nível N. Agora, o tamanho da coordenada de textura de cada texel no nível N é sobreposto ao tamanho de 3 texels no nível N-1.


Um exemplo de tamanho de nível ímpar: 7 texels desse nível são reduzidos para 3 texels no próximo nível. As dimensões das coordenadas de textura dos três texels de alto nível são sobrepostas nos tamanhos dos três texels do nível inferior.

ConseqüentementeDN[i]=min(DN1[2i],DN1[2i+1],DN1[2i+2]). Isso significa que um texel no nível de N-1 às vezes afeta o valor mincalculado para 2 texels no nível de N. Isso é necessário para manter a comparação descrita acima.

A descrição acima foi apresentada em apenas uma dimensão por simplicidade. Em duas dimensões, se as duas dimensões do nível N-1 são pares, a região texel 2x2 no nível N-1 é mapeada para um texel no nível N. Se uma das dimensões for ímpar, a região 2x3 ou 3x2 no nível N-1 é mapeada para uma texel no nível N. Se ambas as dimensões são ímpares , o texel “angular” também deve ser levado em consideração, ou seja, a região 3x3 no nível N-1 é comparada com uma texel no nível N.

Exemplo de código


O código shader GLSL mostrado abaixo implementa o algoritmo que descrevemos. Ele deve ser executado para cada mip subsequente, começando no nível 1 (o nível 0 é o nível de resolução total).

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

Falhas neste código


Primeiro, no caso de buffers de profundidade de resolução total para os quais uma dimensão é mais do que duas vezes o tamanho de outra dimensão, os índices de chamada texelFetchpodem ir além u_depthBuffer. (Nesses casos, a dimensão menor se transforma em 1 antes da outra.) Eu queria usar este exemplo texelFetch(usando coordenadas inteiras) para que o que estivesse acontecendo fosse o mais claro possível e não encontrasse pessoalmente esses buffers particularmente largos / de profundidade alta. Se você encontrar esses problemas, poderá limitar ( clamp) as texelFetchcoordenadas transmitidas ou usar as texturecoordenadas normalizadas da textura (no amostrador, defina um limite na borda). Ao calcular minou maxsempre deve considerar um texel várias vezes para a presença de casos limítrofes.

Em segundo lugar, apesar do fato de as quatro primeiras chamadas texelFetchpoderem ser substituídas por uma textureGather, isso complica as coisas (já que o textureGathernível mip não pode ser indicado); Além disso, não notei um aumento na velocidade ao usar textureGather.

atuação


Usei o shader de fragmento acima para gerar duas cadeias mip completas para dois buffers de profundidade (um para cada olho) no meu mecanismo de VR. No teste, a resolução para cada olho era 1648x1776, o que levou à criação de 10 níveis adicionais de mip reduzidos (o que significa 10 passes). Foram necessários 0,25 ms no NVIDIA GTX 980 e 0,30 ms no AMD R9 290 para gerar uma cadeia completa para os dois olhos.



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

mip-


A tarefa do algoritmo descrito acima é manter a precisão das consultas de profundidade no espaço de coordenadas da textura (ou NDC). Para garantir a integridade (e porque recusei essa garantia na técnica abaixo 2), gostaria de demonstrar mais um método que me deparei (por exemplo, neste artigo ).

Observe que, como o anterior, esse método alternativo foi projetado para funcionar com buffers de resolução total cujos tamanhos não são potências de dois (mas, é claro, eles funcionam com tamanhos iguais à potência de dois).

Nesse método alternativo, ao reduzir a amostragem de um nível com largura (ou altura) ímpar, em vez de adicionar para cada texel de saída uma coluna (ou linha) adicional de texels do nível anterior (inferior), realizamos esta operação apenas para texels de saída com índices máximos (texels "extremos" ) A única coisa que muda no shader de fragmento apresentado acima é definir valores shouldIncludeExtraColumnFromPreviousLevele 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);

Por esse motivo, os texels extremos com o índice mais alto tornam-se muito "espessos", pois cada divisão por 2 dimensões ímpares leva ao fato de ocuparem um intervalo proporcionalmente maior do espaço de coordenadas da textura normalizada.

A desvantagem dessa abordagem é que fica mais difícil executar consultas de profundidade de altos níveis de mip. Em vez de usar apenas as coordenadas de textura normalizadas, primeiro precisamos determinar o texel de resolução total correspondente a essas coordenadas e depois transferir as coordenadas desse texel para as coordenadas do nível mip correspondente, cuja solicitação está sendo executada.

O trecho de código abaixo está migrando do espaço NDC[1,1]2para coordenadas texel no nível 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));

Técnica 2: Gerar um único nível Hi-Z usando um sombreador de computação


A geração de uma cadeia mip completa é bastante rápida, mas me incomodou um pouco que meu aplicativo gere todos esses níveis e use apenas um deles (nível 4). Além de eliminar essa leve ineficiência, também queria ver quanto tudo poderia ser acelerado se eu usasse apenas uma chamada de sombreador de computação para gerar o nível necessário. (Vale a pena notar que meu aplicativo pode parar no nível 4 ao usar uma solução com um shader de fragmentos de várias passagens; portanto, no final desta seção, eu o usei como base para comparar tempos de execução.)

Na maioria dos aplicativos Hi-Z, é necessário apenas um nível de profundidade; portanto, acho essa situação comum. Escrevi um sombreador computacional para meus próprios requisitos específicos (geração de nível 4, que tem uma resolução de 1/16 x 1/16 do original). Código semelhante pode ser usado para gerar níveis diferentes.

O sombreador computacional é adequado para esta tarefa, porque pode usar a memória compartilhada do grupo de trabalho para trocar dados entre threads. Cada grupo de trabalho é responsável por um texel de saída (reduzido por downsampling do buffer), e os threads do grupo de trabalho compartilham o trabalho de calcular os mintexels correspondentes de resolução total, compartilhando os resultados através da memória compartilhada.

Tentei duas soluções principais baseadas em shaders computacionais. No primeiro, cada thread solicitava atomicMinuma variável de memória compartilhada.

Observe que, como os programadores não podem (sem extensões para o hardware de um fabricante específico) executar operações atômicas em valores não inteiros (e minhas profundidades são armazenadas como float), é necessário algum truque aqui. Como os valores de ponto flutuante não negativos do padrão IEEE 754 mantêm sua ordem quando seus bits são processados ​​como valores inteiros não assinados, podemos usar floatBitsToUintpara trazer (usando reinterpretar a conversão) os valores de profundidade floatpara uinte, em seguida, chamar atomicMin(para executar uintBitsToFloato valor mínimo final uint) .

A solução mais óbvia atomicMinseria criar grupos de threads 16x16 nos quais cada thread recebe um texel e, em seguida, executa-o atomicMincom um valor na memória compartilhada. Comparei essa abordagem usando blocos de fluxo menores (8x8, 4x8, 4x4, 2x4, 2x2), nos quais cada fluxo recebe uma região texel e calcula seu próprio mínimo local e depois chama atomicMin.

A mais rápida de todas essas soluções testadas comatomicMinA NVIDIA e a AMD acabaram tendo uma solução com blocos de fluxo 4x4 (nos quais cada fluxo recebe uma área texel 4x4). Não entendo muito bem por que essa opção acabou sendo a mais rápida, mas talvez ela reflita um compromisso entre a competição de operações atômicas e os cálculos em fluxos independentes. Também é importante notar que o tamanho do grupo de trabalho 4x4 usa apenas 16 threads por warp / wave (e é possível usar também 32 ou 64), o que é interessante. O exemplo abaixo implementa essa abordagem.

Como alternativa ao uso, atomicMintentei realizar uma redução paralela usando as técnicas usadas nesta apresentação da NVIDIA ativamente citada.. (A idéia básica é usar uma matriz de memória compartilhada do mesmo tamanho que o número de threads no grupo de trabalho, bem comolog2(n)passa para o cálculo conjunto seqüencial dos minmínimos de cada fluxo até que o mínimo final de todo o grupo de trabalho seja obtido.)

Tentei esta solução com todos os mesmos tamanhos de grupos de trabalho da solução c atomicMin. Mesmo com todas as otimizações descritas na apresentação da NVIDIA, a solução de redução paralela é um pouco mais lenta (dentro de dez GPUs em ambas as GPUs) do que a solução atomicMinque encontrei. Além disso, a solução com é atomicMinmuito mais simples em termos de código.

Exemplo de código


Com esse método, a maneira mais fácil é não tentar manter a correspondência no espaço normalizado das coordenadas de textura entre texels de buffers reduzidos e resolução total. Você pode simplesmente realizar conversões de coordenadas texel de resolução total em coordenadas texel de resolução reduzida:

ivec2 reducedResTexelCoords = texelCoords / ivec2(downscalingFactor);

No meu caso (gerar o equivalente ao nível mip 4) downscalingFactoré 16.

Como mencionado acima, esse shader computacional GLSL implementa uma solução com atomicMintamanhos de grupo de trabalho 4x4, em que cada thread recebe uma área texel 4x4 do buffer de resolução total. O buffer de profundidade de valor reduzido resultante miné 1/16 x 1/16 do tamanho do buffer de resolução total (arredondado para cima quando os tamanhos de resolução total não são divisíveis por 16 completamente).

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

atuação


Usei o shader computacional acima para processar o buffer de profundidade de resolução total com as mesmas dimensões que foram usadas para gerar a cadeia mip completa (buffers de 1648x1776 para cada olho). Ele roda em 0,12 ms no NVIDIA GTX 980 e 0,08 ms no AMD R9 290. Se compararmos com o tempo de geração de apenas 1 a 4 níveis de mip (0,22 ms no NVIDIA, 0,25 ms AMD), A solução com um shader computacional acabou sendo 87% mais rápida com as GPUs NVIDIA e 197% mais rápida que as GPUs AMD .

Em termos absolutos, a aceleração não é tão grande, mas a cada 0,1 ms é importante, especialmente em VR :)

All Articles