Tampon de profondeur hiérarchique


Bref avis


Un tampon de profondeur hiérarchique est un tampon de profondeur à plusieurs niveaux (Z-buffer) utilisé comme structure d'accélération pour les requêtes de profondeur. Comme dans le cas des chaînes de mip de texture, les tailles de chaque niveau sont généralement le résultat de la division par le degré de deux des tailles du tampon pleine résolution. Dans cet article, je vais parler de deux façons de générer un tampon de profondeur hiérarchique à partir d'un tampon pleine résolution.

Tout d'abord, je vais vous montrer comment générer une chaîne de mip complète pour un tampon de profondeur, qui préserve la précision des requêtes de profondeur dans l'espace de coordonnées de texture (ou NDC) même pour des tailles de tampon de profondeur non égales à des puissances de deux. (Sur Internet, j'ai rencontré des exemples de code qui ne garantissent pas cette précision, ce qui complique l'exécution de requêtes précises à des niveaux de mip élevés.)

Ensuite, pour les cas où un seul niveau de sous-échantillonnage est requis, je montrerai comment générer ce niveau avec un seul appel à un shader de calcul à l'aide d'opérations atomiques dans la mémoire partagée du groupe de travail. Pour mon application, où seule une résolution de 1/16 x 1/16 est requise (niveau de mip 4), la méthode avec un shader de calcul est 2-3 fois plus rapide que l'approche habituelle avec sous-échantillonnage de la chaîne de mip en plusieurs passes.

introduction


Les profondeurs hiérarchiques (également appelées Hi-Z) est une technique souvent utilisée dans les graphiques 3D. Il est utilisé pour accélérer le découpage de la géométrie invisible (élimination des occlusions) (dans le CPU , ainsi que dans le GPU ), le calcul des réflexions dans l'espace de l'écran , le brouillard volumétrique et bien plus encore.

De plus, les GPU Hi-Z sont souvent implémentés dans le cadre d'un pipeline de rastérisation. Les opérations de recherche rapide Hi-Z dans les caches sur la puce vous permettent d'ignorer complètement les fragments de fragments s'ils sont complètement couverts par des primitives précédemment rendues.

L'idée de base de Hi-Z est d'accélérer les opérations de requête en profondeur en lisant à partir de tampons de résolution inférieure. Ceci est plus rapide que la lecture des profondeurs en pleine résolution à partir du tampon, pour deux raisons:

  1. Un texel (ou seulement quelques texels) d'un tampon à résolution inférieure peut être utilisé comme valeur approximative d'une pluralité de texels d'un tampon à haute résolution.
  2. Un tampon de résolution inférieure peut être suffisamment petit et mis en cache, ce qui accélère considérablement l'exécution des opérations de recherche (en particulier avec un accès aléatoire).

Le contenu des niveaux de tampon Hi-Z sous-échantillonnés dépend de la façon dont ils sont utilisés (si le tampon de profondeur sera «inversé» , quels types de requêtes doivent être utilisés). En général, un texel au niveau du tampon Hi-Z stocke minou maxtous les texels qui lui correspondent au niveau précédent. Parfois, les valeurs de minet sont stockées en même temps max. Les valeurs moyennes simples (qui sont souvent utilisées dans les niveaux de mip des textures régulières) sont rarement utilisées car elles sont rarement utiles pour de tels types de requêtes.

Les tampons Hi-Z sont le plus souvent demandés presque immédiatement à la sortie pour éviter un traitement ultérieur et des opérations de recherche plus précises dans le tampon pleine résolution. Par exemple, si nous stockons des valeursmaxpour un tampon de profondeur non inversé (dans lequel plus la valeur de profondeur est élevée, plus l'objet est éloigné), nous pouvons rapidement déterminer exactement si une position particulière dans l'espace d'écran est couverte par un tampon de profondeur (si sa coordonnée Z> est la valeur (max) stockée dans certains niveau supérieur (c.-à-d., résolution inférieure) du tampon Hi-Z).

Veuillez noter que j'ai utilisé l'expression «exactement»: si la coordonnée Z <= la valeur reçue (max), alors on ne sait pas si son tampon se chevauche. Dans certaines applications, en cas d'incertitude, il peut être nécessaire de rechercher dans le tampon des profondeurs de pleine résolution; dans d'autres cas, cela n'est pas nécessaire (par exemple, si seuls des calculs inutiles sont en jeu, et non le rendu correct).

Mon application: rendre des particules dans un shader de calcul


J'étais confronté à la nécessité d'utiliser Hi-Z lors de l'implémentation du rendu de particules dans un shader de calcul dans le moteur de mon application PARTICULATE VR . Étant donné que cette technique de rendu n'utilise pas la pixellisation avec des fonctions fixes, elle doit utiliser sa propre vérification de profondeur pour chaque particule d'une taille d'un pixel. Et comme les particules ne sont en aucun cas triées, l'accès au tampon de profondeur est (dans le pire des cas) presque aléatoire.

Les opérations de recherche sur une texture à accès aléatoire en plein écran sont le moyen de mauvaises performances. Pour réduire la charge, je cherche d'abord les profondeurs dans le tampon de profondeur réduite avec une résolution de 1/16 x 1/16 par rapport à l'original. Ce tampon contient des valeurs de profondeur.min, qui permet au shader de rendu de calcul pour la grande majorité des particules visibles de sauter le test de profondeur en pleine résolution. (Si la profondeur des particules est <la profondeur minimale stockée dans le tampon de résolution inférieure, alors nous savons qu'elle est absolument visible. Si elle> = min, alors nous devons vérifier le tampon de profondeur pleine résolution.)

Grâce à cela, le test de profondeur pour les particules visibles dans le cas général devient une opération à faible coût. (Pour les particules superposées par la géométrie, c'est plus cher, mais cela nous convient car cela n'entraîne pas de coûts de rendu, donc les particules nécessitent encore peu de calcul.)

Du fait que la recherche est d'abord effectuée dans le tampon de profondeurs de résolution inférieure (comme mentionné ci-dessus) , le temps de rendu des particules est réduit de 35% maximumpar rapport au cas où la recherche est effectuée uniquement dans le tampon pleine résolution. Par conséquent, pour mon application, Hi-Z est très bénéfique.

Nous allons maintenant examiner deux techniques pour générer un tampon de profondeur hiérarchique.

Technique 1: génération d'une chaîne Mip complète


Dans de nombreuses applications Hi-Z, la création d'une chaîne complète de mip de tampon de profondeur est requise. Par exemple, lorsque vous effectuez une élimination d'occlusion à l'aide de Hi-Z, le volume de délimitation est projeté dans l'espace d'écran et la taille projetée est utilisée pour sélectionner le niveau de mip approprié (de sorte qu'un nombre fixe de texels est impliqué dans chaque test de chevauchement).

La génération d'une chaîne mip à partir du tampon de profondeur pleine résolution est généralement une tâche simple - pour chaque texel au niveau N, nous prenons max( minou les deux) les 4 texels correspondants dans le niveau N-1 précédemment généré. Nous effectuons des passes séquentielles (chaque fois en réduisant la taille de moitié) jusqu'à ce que nous obtenions le dernier niveau de mip 1x1.

Cependant, dans le cas des tampons de profondeur, dont les tailles ne correspondent pas aux puissances de deux, tout est plus compliqué. Étant donné que Hi-Z pour les tampons de profondeur est souvent construit à partir de résolutions d'écran standard (qui sont rarement des puissances de deux), nous devons trouver une solution fiable à ce problème.

Décidons d'abord ce que signifiera la valeur de chaque tampon de profondeur de niveau mip texel. Dans cet article, nous supposerons que la chaîne mip stocke des valeurs min. Les opérations de recherche de profondeur doivent utiliser le filtrage des voisins les plus proches, car l'interpolation des valeurs est mininutile pour nous et nuira à la nature hiérarchique de la chaîne mip de profondeurs créée.

Alors, que signifiera exactement la valeur du texel individuel au niveau mip N obtenu par nous? Cela devrait être la valeur minimale (min) de tous les texels du tampon de profondeur plein écran qui occupe le même espace dans l'espace de coordonnées de texture (normalisé).

En d'autres termes, si une coordonnée distincte de la texture (dans l'intervalle ) est mappé (en filtrant les voisins les plus proches) sur un texel individuel du tampon pleine résolution, alors ce texel de pleine résolution doit être considéré comme un candidat pour la valeurcalculée pour le texel à chaque niveau de mip supérieur suivant avec lequel la même coordonnée est mappée textures. Si cette correspondance est garantie, alors nous serons sûrs que l'opération de recherche à des niveaux de mip élevés ne renverra jamais la valeur de profondeur> valeur de texel dans la même coordonnée de texture correspondant au tampon pleine résolution (niveau 0). Dans le cas d'un N séparé, cette garantie est maintenue pour tous les niveaux inférieurs (<N).[0,1]2min



Pour les dimensions paires (et dans le cas de tampons pleine résolution, qui sont des puissances de deux, les dimensions paires à chaque niveau jusqu'au dernier, où les dimensions deviennent égales à 1) seront faciles à faire. Dans le cas unidimensionnel, pour le texel avec un indicei au niveau N, nous devons prendre des texels au niveau N-1 avec des indices2 et2i+1 et trouvez leur valeurmin. C'est à direDN[i]=min(DN1[2i],DN1[2i+1]) . Nous pouvons comparer directement les texels dans le rapport «2 pour 1» (et donc la taille des coordonnées de texture), car la taille de chaque niveau est exactement deux fois plus petite que la précédente.


Un exemple de tailles de niveau égales: 6 texels à ce niveau sont réduits à 3 à un niveau supérieur. Les tailles de coordonnées de texture de chacun des trois texels de haut niveau sont précisément superposées à tous les deux texels de niveau inférieur. (Les points sont les centres des texels, et les carrés sont les dimensions des coordonnées de texture lors du filtrage des voisins les plus proches.)

Dans le cas de tailles de niveau impair (et les tampons de pleine résolution qui ne sont pas une puissance de deux auront au moins un niveau avec une taille impaire) Devient plus dur. Pour le niveau N-1 de taille impaire taille du niveau suivant (N) sera égaledimN1dimN=dimN12, c'est-à-diredimN12 .

Cela signifie que maintenant nous n'avons pas de mappage clair de 2 à 1 des texels de niveau N-1 aux texels de niveau N. Maintenant, la taille de la coordonnée de texture de chaque texel au niveau N est superposée à la taille de3texels au niveau N-1.


Un exemple de taille de niveau impair: 7 texels de ce niveau sont réduits à 3 texels au niveau suivant. Les dimensions des coordonnées de texture des trois texels de haut niveau se superposent aux tailles des trois texels du niveau inférieur.

Par conséquentDN[i]=min(DN1[2i],DN1[2i+1],DN1[2i+2]) . Cela signifie qu'un texel au niveau de N-1 affecte parfois la valeurmincalculée pour2texels au niveau de N. Ceci est nécessaire pour maintenir la comparaison décrite ci-dessus.

La description ci-dessus a été présentée dans une seule dimension pour plus de simplicité. En deux dimensions, si les deux dimensions du niveau N-1 sont paires, alors la région de texel 2x2 au niveau N-1 est mappée à un texel au niveau N. Si l'une des dimensions est impaire, la région 2x3 ou 3x2 au niveau N-1 est mappée à une texel au niveau N. Si les deux dimensions sont impaires , alors le texel «angulaire» doit également être pris en compte, c'est-à-dire que la région 3x3 au niveau N-1 est comparée à un texel au niveau N.

Exemple de code


Le code de shader GLSL illustré ci-dessous implémente l'algorithme que nous avons décrit. Il doit être exécuté pour chaque mip suivant, à partir du niveau 1 (le niveau 0 est le niveau de pleine résolution).

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

Failles dans ce code


Premièrement, dans le cas de tampons de profondeur pleine résolution pour lesquels une dimension est plus de deux fois la taille d'une autre dimension, les indices d'appel texelFetchpeuvent aller au-delà u_depthBuffer. (Dans de tels cas, la plus petite dimension se transforme en 1 avant l'autre.) Je voulais utiliser dans cet exemple texelFetch(en utilisant des coordonnées entières), afin que ce qui se passait soit aussi clair que possible et ne rencontre pas personnellement de tels tampons particulièrement larges / profonds. Si vous rencontrez de tels problèmes, vous pouvez limiter ( clamp) les texelFetchcoordonnées transmises ou utiliser les texturecoordonnées normalisées de la texture (dans l'échantillonneur, définissez une limite sur le bord). Lors du calcul minou maxdoit toujours considérer un texel plusieurs fois pour la présence de cas limites.

Deuxièmement, malgré le fait que les quatre premiers appels texelFetchpuissent être remplacés par un textureGather, cela complique les choses (puisque le textureGatherniveau de mip ne peut pas être indiqué); De plus, je n'ai pas remarqué d'augmentation de vitesse lors de l'utilisation textureGather.

Performance


J'ai utilisé le fragment shader ci-dessus pour générer deux chaînes de mip complètes pour deux (un pour chaque œil) tampons de profondeur dans mon moteur VR. Dans le test, la résolution de chaque œil était de 1648x1776, ce qui a conduit à la création de 10 niveaux de mip réduits supplémentaires (ce qui signifie 10 passes). Il a fallu 0,25 ms sur la NVIDIA GTX 980 et 0,30 ms sur l'AMD R9 290 pour générer une chaîne complète pour les deux yeux.



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

mip-


La tâche de l'algorithme décrit ci-dessus est de maintenir la précision des requêtes de profondeur dans l'espace de coordonnées de texture (ou NDC). Pour être complet (et parce que j'ai refusé cette garantie dans la technique ci-dessous 2), je voudrais démontrer une autre méthode que j'ai rencontrée (par exemple, dans cet article ).

Notez que, comme la précédente, cette méthode alternative est conçue pour fonctionner avec des tampons pleine résolution dont les tailles ne sont pas des puissances de deux (mais, bien sûr, elles fonctionnent avec des tailles égales à des puissances de deux).

Dans cette méthode alternative, lors du sous-échantillonnage d'un niveau avec une largeur (ou une hauteur) impaire au lieu d'ajouter une colonne (ou une ligne) supplémentaire de texels du niveau précédent (inférieur) pour chaque texel de sortie, nous effectuons cette opération uniquement pour les texels de sortie avec des indices maximum (texels «extrêmes» ) La seule chose qui change dans le fragment shader présenté ci-dessus est la définition de valeurs shouldIncludeExtraColumnFromPreviousLevelet 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);

De ce fait, les texels extrêmes avec l'indice le plus élevé deviennent très «épais», car chaque division par 2 dimensions impaires conduit au fait qu'ils occupent un intervalle proportionnellement plus grand de l'espace de coordonnées de texture normalisé.

L'inconvénient de cette approche est qu'il devient plus difficile d'effectuer des requêtes de profondeur de niveaux de mip élevés. Au lieu d'utiliser simplement les coordonnées de texture normalisées, nous devons d'abord déterminer le texel de pleine résolution correspondant à ces coordonnées, puis transférer les coordonnées de ce texel aux coordonnées du niveau de mip correspondant, dont la demande est en cours d'exécution.

L'extrait de code ci-dessous migre de l'espace NDC[1,1]2 aux coordonnées texel au niveau du 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));

Technique 2: générer un niveau Hi-Z unique à l'aide d'un nuanceur informatique


La génération d'une chaîne de mip complète est assez rapide, mais cela m'a un peu dérangé que mon application génère tous ces niveaux et n'utilise qu'un seul d'entre eux (niveau 4). En plus d'éliminer cette légère inefficacité, je voulais aussi voir à quel point tout pourrait être accéléré si je n'utilisais qu'un seul appel de shader de calcul pour générer le niveau dont j'avais besoin. (Il convient de noter que mon application peut s'arrêter au niveau 4 lors de l'utilisation d'une solution avec un shader de fragment à passages multiples, donc à la fin de cette section, je l'ai utilisée comme base pour comparer les temps d'exécution.)

Dans la plupart des applications Hi-Z, un seul niveau de profondeur est requis, donc je trouve cette situation courante. J'ai écrit un shader de calcul pour mes propres besoins spécifiques (génération de niveau 4, qui a une résolution de 1/16 x 1/16 par rapport à l'original). Un code similaire peut être utilisé pour générer différents niveaux.

Le shader de calcul est bien adapté à cette tâche, car il peut utiliser la mémoire de groupe de travail partagée pour échanger des données entre les threads. Chaque groupe de travail est responsable d'un texel de sortie (réduit par le sous-échantillonnage du tampon), et les threads du groupe de travail partagent le travail de calcul des mintexels correspondants de pleine résolution, partageant les résultats via la mémoire partagée.

J'ai essayé deux solutions principales basées sur des shaders informatiques. Dans le premier, chaque thread a appelé atomicMinune variable de mémoire partagée.

Veuillez noter que puisque les programmeurs ne peuvent pas (sans extensions pour le matériel d'un fabricant particulier) effectuer des opérations atomiques sur des valeurs non entières (et mes profondeurs sont stockées comme float), une astuce est nécessaire ici. Étant donné que les valeurs à virgule flottante non négatives de la norme IEEE 754 conservent leur ordre lorsque leurs bits sont traités comme des valeurs entières non signées, nous pouvons utiliser floatBitsToUintpour apporter (à l'aide de la fonte de réinterprétation) les valeurs de profondeur floatà uint, puis appeler atomicMin(pour ensuite exécuter uintBitsToFloatpour la valeur minimale finie uint) .

La solution la plus évidente atomicMinserait de créer des groupes de threads 16x16 dans lesquels chaque thread reçoit un texel puis l'exécute atomicMinavec une valeur dans la mémoire partagée. J'ai comparé cette approche en utilisant des blocs de flux plus petits (8x8, 4x8, 4x4, 2x4, 2x2), dans lesquels chaque flux reçoit une région de texels et calcule son propre minimum local, puis appelle atomicMin.

La plus rapide de toutes ces solutions testées avecatomicMinNVIDIA et AMD se sont avérés avoir une solution avec des blocs de flux 4x4 (dans lesquels chaque flux lui-même reçoit une zone de texel 4x4). Je ne comprends pas très bien pourquoi cette option s'est avérée la plus rapide, mais elle reflète peut-être un compromis entre la concurrence des opérations atomiques et les calculs en flux indépendants. Il convient également de noter que la taille du groupe de travail 4x4 n'utilise que 16 threads par chaîne / onde (et il est possible d'utiliser également 32 ou 64), ce qui est intéressant. L'exemple ci-dessous met en œuvre cette approche.

Comme alternative à l'utilisation, atomicMinj'ai essayé d'effectuer une réduction parallèle en utilisant les techniques utilisées dans cette présentation NVIDIA activement citée.. (L'idée de base est d'utiliser un tableau de mémoire partagée de la même taille que le nombre de threads dans le groupe de travail, ainsi quelog2(n) passe pour le calcul conjoint séquentiel desmincreux de chaque fil jusqu'à ce que le minimum final de tout le groupe de travail soit obtenu.)

J'ai essayé cette solution avec toutes les mêmes tailles de groupe de travail que dans la solution catomicMin. Même avec toutes les optimisations décrites dans la présentation NVIDIA, la solution de réduction parallèle est légèrement plus lente (dans les dix GPU sur les deux GPU) que la solution à laquelleatomicMinje suis arrivé. De plus, la solution avec estatomicMinbeaucoup plus simple en termes de code.

Exemple de code


Avec cette méthode, le moyen le plus simple est de ne pas essayer de maintenir la correspondance dans l'espace normalisé des coordonnées de texture entre les texels de tampons réduits et la pleine résolution. Vous pouvez simplement effectuer des conversions de coordonnées texel de pleine résolution en coordonnées texel de résolution réduite:

ivec2 reducedResTexelCoords = texelCoords / ivec2(downscalingFactor);

Dans mon cas (générer l'équivalent de mip-niveau 4) downscalingFactorest 16.

Comme mentionné ci-dessus, ce shader de calcul GLSL implémente une solution avec atomicMindes tailles de groupe de travail 4x4, où chaque thread reçoit une zone de texel 4x4 du tampon pleine résolution. Le tampon de profondeur de valeur réduite résultant minest 1/16 x 1/16 de la taille du tampon pleine résolution (arrondi lorsque les tailles pleine résolution ne sont pas divisibles par 16 complètement).

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

Performance


J'ai utilisé le nuanceur de calcul ci-dessus pour traiter le tampon de profondeur pleine résolution avec les mêmes dimensions que celles utilisées pour générer la chaîne de mip complète (tampons 1648x1776 pour chaque œil). Il s'exécute en 0,12 ms sur le NVIDIA GTX 980 et 0,08 ms sur l'AMD R9 290. Si nous comparons avec le temps de génération des niveaux de mip uniquement 1 à 4 (0,22 ms sur NVIDIA, 0,25 ms AMD), alors La solution avec un shader de calcul s'est avérée 87% plus rapide avec les GPU NVIDIA et 197% plus rapide que les GPU AMD .

En termes absolus, l'accélération n'est pas si grande, mais toutes les 0,1 ms sont importantes, surtout en VR :)

All Articles