Texture convolutionnelle

Textures à mise à jour automatique


Lorsqu'il est possible de paralléliser des simulations ou des tâches de rendu, il est généralement préférable de les exécuter dans le GPU. Dans cet article, je vais expliquer une technique qui utilise ce fait pour créer des trucs visuels impressionnants avec des frais généraux de faible performance. Tous les effets que je vais démontrer sont implémentés à l'aide de textures qui, une fois mises à jour, se " rendent "; la texture est mise à jour lorsqu'un nouveau cadre est rendu et l'état de texture suivant dépend complètement de l'état précédent. Sur ces textures, vous pouvez dessiner, provoquant certains changements, et la texture elle-même, directement ou indirectement, peut être utilisée pour rendre des animations intéressantes. Je les appelle des textures convolutionnelles .


Figure 1: double tampon de convolution

Avant de continuer, nous devons résoudre un problème: la texture ne peut pas être lue et écrite en même temps, des API graphiques comme OpenGL et DirectX ne le permettent pas. Étant donné que l'état suivant de la texture dépend du précédent, nous devons en quelque sorte contourner cette limitation. J'ai besoin de lire à partir d'une texture différente, pas de celle dans laquelle j'écris.

La solution est un double tampon . La figure 1 montre comment cela fonctionne: en fait, au lieu d'une texture, il y en a deux, mais l'une est écrite et l'autre est lue de l'autre. La texture en cours d'écriture est appelée le tampon arrière et la texture rendue est appelée le tampon avant . Étant donné que le test convolutionnel est «écrit sur lui-même», le tampon secondaire de chaque trame écrit dans le tampon principal, puis le primaire est rendu ou utilisé pour le rendu. Dans la trame suivante, les rôles changent et le tampon primaire précédent est utilisé comme source pour le tampon primaire suivant.

En rendant l'état précédent dans une nouvelle texture de convolution en utilisant le shader de fragment (ou pixel shader ) fournit des effets et des animations intéressants. Le shader détermine comment l'état change. Le code source de tous les exemples de l'article (ainsi que d'autres) peut être trouvé dans le référentiel sur GitHub .

Exemples d'application simples


Pour démontrer cette technique, j'ai choisi une simulation bien connue dans laquelle, lors de la mise à jour, l'état dépend complètement de l'état précédent: le jeu Conway «Life» . Cette simulation est réalisée dans une grille de carrés dont chaque cellule est vivante ou morte. Les règles pour l'état de cellule suivant sont simples:

  • Si une cellule vivante a moins de deux voisins, mais elle devient morte.
  • Si une cellule vivante a deux ou trois voisins vivants, elle reste vivante.
  • Si une cellule vivante a plus de trois voisins vivants, elle devient morte.
  • Si une cellule morte a trois voisins vivants, elle devient vivante.

Pour implémenter ce jeu comme une texture convolutive, j'interprète la texture comme la grille du jeu, et le rendu du shader est basé sur les règles ci-dessus. Un pixel transparent est une cellule morte et un pixel blanc opaque est une cellule vivante. Une implémentation interactive est illustrée ci-dessous. Pour accéder au GPU, j'utilise myr.js , qui nécessite WebGL 2 . La plupart des navigateurs modernes (par exemple, Chrome et Firefox) peuvent fonctionner avec, mais si la démo ne fonctionne pas, le navigateur ne la prend probablement pas en charge. Utilisez la souris (ou l'écran tactile) [dans l'article original] pour dessiner des cellules vivantes sur la texture.


Le code du fragment shader (dans GLSL, car j'utilise WebGL pour le rendu) est illustré ci-dessous. Tout d'abord, j'implémente la fonction get , qui me permet de lire un pixel à partir d'un décalage spécifique par rapport à celui en cours. La variable pixelSize est un vecteur 2D pré-créé contenant le décalage UV de chaque pixel, et la fonction get l'utilise pour lire la cellule voisine. Ensuite, la fonction main détermine la nouvelle couleur de la cellule en fonction de l'état actuel (en live ) et du nombre de voisins vivants.

 uniform sampler2D source; uniform lowp vec2 pixelSize; in mediump vec2 uv; layout (location = 0) out lowp vec4 color; int get(int dx, int dy) { return int(texture(source, uv + pixelSize * vec2(dx, dy)).r); } void main() { int live = get(0, 0); int neighbors = get(-1, -1) + get(0, -1) + get(1, -1) + get(-1, 0) + get(1, 0) + get(-1, 1) + get(0, 1) + get(1, 1); if (live == 1 && neighbors < 2) color = vec4(0); else if (live == 1 && (neighbors == 2 || neighbors == 3)) color = vec4(1); else if (live == 1 && neighbors == 3) color = vec4(0); else if (live == 0 && neighbors == 3) color = vec4(1); else color = vec4(0); } 

Une autre texture convolutionnelle simple est un jeu avec du sable qui tombe , dans lequel l'utilisateur peut jeter du sable coloré sur la scène, qui tombe et forme des montagnes. Bien que sa mise en œuvre soit un peu plus compliquée, les règles sont plus simples:

  • S'il n'y a pas de sable sous un grain de sable, il tombe d'un pixel vers le bas.
  • S'il y a du sable sous un grain de sable, mais qu'il peut glisser de 45 degrés vers la gauche ou la droite, il le fera.

La gestion dans cet exemple est la même que dans le jeu "Life". Étant donné que dans de telles règles, le sable peut tomber à une vitesse d'un seul pixel par image afin d'accélérer légèrement le processus, la texture par image est mise à jour trois fois. Le code source de l'application est ici .


Un pas en avant


ChaîneCandidature
RougeHauteur des vagues
VertVitesse des vagues
BleuNon utilisé
AlphaNon utilisé

Figure 2: vagues de pixels.

Les exemples ci-dessus utilisent directement la texture convolutionnelle; son contenu est restitué à l'écran tel quel. Si vous interprétez les images uniquement comme des pixels, les limites d'utilisation de cette technique sont très limitées, mais grâce à un équipement moderne, elles peuvent être étendues. Au lieu de compter les pixels comme des couleurs, je les interpréterai un peu différemment, ce qui peut être utilisé pour créer des animations d'une autre texture ou d'un modèle 3D.

Tout d'abord, je vais interpréter la texture convolutionnelle comme une carte de hauteur. La texture simulera les vagues et les vibrations sur le plan d'eau, et les résultats seront utilisés pour rendre les réflexions et les vagues ombrées. Nous ne sommes plus tenus de lire la texture comme une image, nous pouvons donc utiliser ses pixels pour stocker des informations. Dans le cas d'un shader à eau, je stocke la hauteur des vagues dans le canal rouge et l'impulsion des vagues dans le canal vert, comme illustré à la figure 2. Les canaux bleu et alpha ne sont pas encore utilisés. Les vagues sont créées en dessinant des taches rouges sur une texture convolutionnelle.

Je ne considérerai pas la méthodologie de mise à jour de la carte des hauteurs, que j'ai empruntée au site d' Hugo Elias , qui semble avoir disparu d'Internet. Il a également découvert cet algorithme d'un auteur inconnu et l'a implémenté en C pour exécution dans le CPU. Le code source de l'application ci-dessous est ici .


Ici, j'ai utilisé une carte de hauteur uniquement pour compenser la texture et ajouter de l'ombrage, mais dans la troisième dimension, des applications beaucoup plus intéressantes peuvent être implémentées. Lorsqu'une texture convolutionnelle est interprétée par un vertex shader, un plan plat subdivisé peut être déformé pour créer des ondes tridimensionnelles. Vous pouvez appliquer l'ombrage et l'éclairage habituels à la forme résultante.

Il convient de noter que les pixels de la texture convolutionnelle de l'exemple ci-dessus stockent parfois de très petites valeurs qui ne devraient pas disparaître en raison d'erreurs d'arrondi. Par conséquent, les canaux de couleur de cette texture doivent avoir une résolution plus élevée et non les 8 bits standard. Dans cet exemple, j'ai augmenté la taille de chaque canal de couleur à 16 bits, ce qui a donné des résultats assez précis. Si vous ne stockez pas de pixels, vous devez souvent augmenter la précision de la texture. Heureusement, les API graphiques modernes prennent en charge cette fonctionnalité.

Nous utilisons tous les canaux


ChaîneCandidature
RougeDécalage X
VertDécalage Y
BleuVitesse X
AlphaDécalage Y

Figure 3: Pixel grass.

Dans l'exemple de l'eau, seuls les canaux rouge et vert sont utilisés, mais dans l'exemple suivant, nous appliquerons les quatre. Un champ avec de l'herbe (ou des arbres) est simulé, qui peut être déplacé à l'aide du curseur. La figure 3 montre quelles données sont stockées dans un pixel. Le décalage est stocké dans les canaux rouge et vert et la vitesse est stockée dans les canaux bleu et alpha. Cette vitesse est mise à jour pour se déplacer vers la position de repos avec un mouvement d'onde qui s'estompe progressivement.

Dans l'exemple avec de l'eau, créer des vagues est assez simple: des taches peuvent être dessinées sur la texture et le mélange alpha fournit des formes douces. Vous pouvez facilement créer plusieurs spots qui se chevauchent. Dans cet exemple, tout est plus délicat car le canal alpha est déjà utilisé. Nous ne pouvons pas dessiner un point avec une valeur alpha de 1 au centre et 0 du bord, car cela donnera à l'herbe une impulsion inutile (car l'impulsion verticale est stockée dans le canal alpha). Dans ce cas, un ombrage séparé a été écrit pour dessiner l'effet sur la texture convolutionnelle. Ce shader garantit que le mélange alpha ne produit pas d'effets inattendus.

Le code source de l'application se trouve ici .


L'herbe est créée en 2D, mais l'effet fonctionnera dans des environnements 3D. Au lieu du déplacement des pixels, les sommets sont décalés, ce qui est également plus rapide. De plus, à l'aide de pics, un autre effet peut être réalisé: force différente des branches - l'herbe se plie facilement au moindre vent et les arbres forts ne fluctuent que pendant les tempêtes.

Bien qu'il existe de nombreux algorithmes et shaders pour créer les effets du vent et du déplacement de la végétation, cette approche a un sérieux avantage: dessiner des effets sur une texture convolutionnelle est un processus très peu coûteux. Si l'effet est appliqué dans un jeu, alors le mouvement de la végétation peut être déterminé par des centaines d'influences différentes. Non seulement le personnage principal, mais aussi tous les objets, animaux et mouvements peuvent influencer le monde au détriment de coûts insignifiants.

Autres cas d'utilisation et défauts


Vous pouvez proposer de nombreuses autres applications technologiques, par exemple:

  • En utilisant une texture convolutionnelle, vous pouvez simuler la vitesse du vent. Sur la texture, vous pouvez dessiner des obstacles qui font circuler l'air autour d'eux. Les particules (pluie, neige et feuilles) peuvent utiliser cette texture pour contourner les obstacles.
  • Vous pouvez simuler la propagation de la fumée ou du feu.
  • La texture peut coder l'épaisseur d'une couche de neige ou de sable. Les traces et autres interactions avec le calque peuvent créer des bosses et des impressions sur le calque.

Lors de l'utilisation de cette méthode, il existe des difficultés et des limites:

  • Il est difficile d'ajuster les animations à l'évolution des fréquences d'images. Par exemple, dans une application avec du sable qui tombe, les grains de sable tombent à une vitesse constante - un pixel par mise à jour. Une solution possible peut être de mettre à jour les textures convolutionnelles avec une fréquence constante, semblable à la façon dont la plupart des moteurs physiques fonctionnent; le moteur physique fonctionne à une fréquence constante et ses résultats sont interpolés.
  • Le transfert de données vers le GPU est un processus rapide et facile, cependant, récupérer des données n'est pas si facile. Cela signifie que la plupart des effets générés par cette technique sont unidirectionnels; ils sont transférés vers le GPU, et le GPU fait son travail sans autre intervention et rétroaction. Si je voulais intégrer la longueur d'onde de l'exemple de l'eau dans des calculs physiques (par exemple, pour que les navires oscillent avec les vagues), alors j'aurais besoin de valeurs de la texture convolutionnelle. La récupération des données de texture à partir d'un GPU est un processus extrêmement lent qui n'a pas besoin d'être effectué en temps réel. La solution à ce problème peut être l'implémentation de deux simulations: l'une avec une haute résolution pour les graphiques de l'eau en tant que texture convolutionnelle, l'autre avec une basse résolution dans le CPU pour la physique de l'eau. Si les algorithmes sont les mêmes, les écarts peuvent être tout à fait acceptables.

Les démos de cet article peuvent être encore optimisées. Dans l'exemple de l'herbe, vous pouvez utiliser une texture avec une résolution beaucoup plus faible sans défauts notables; cela aidera beaucoup dans les grandes scènes. Autre optimisation: vous pouvez utiliser un taux de rafraîchissement inférieur, par exemple, dans chaque quatrième trame, ou un quart par trame (car cette technique ne pose pas de problème avec les mises à jour segmentées). Pour maintenir une fréquence d'images régulière, l'état précédent et actuel de la texture convolutionnelle peut être interpolé.

Étant donné que les textures convolutives utilisent une double mémoire tampon interne, vous pouvez utiliser les deux textures en même temps pour le rendu. Le tampon principal est l'état actuel et le secondaire est le précédent. Cela peut être utile pour interpoler la texture dans le temps ou pour calculer des dérivés pour les valeurs de texture.

Conclusion


Les GPU, en particulier dans les programmes 2D, sont souvent inactifs. Bien qu'il semble qu'elle ne puisse être utilisée que pour le rendu de scènes 3D complexes, la technique présentée dans cet article montre au moins une autre façon d'utiliser la puissance du GPU. En utilisant les capacités pour lesquelles le GPU a été développé, vous pouvez implémenter des effets et des animations intéressants qui sont généralement trop coûteux pour le CPU.

Source: https://habr.com/ru/post/fr478772/


All Articles