L'algorithme d'interaction de centaines de milliers de particules uniques sur le GPU, dans GLES3 et WebGL2

Description de l'algorithme logique et analyse d'un exemple de travail sous la forme d'un jeu techno-démo


Version WebGL2 de cette démo https://danilw.itch.io/flat-maze-web pour d' autres liens, voir l'article.



L'article est divisé en deux parties, d'abord sur la logique, et la deuxième partie sur l'application dans le jeu, la première partie :


  • Caractéristiques clés
  • Liens et brève description.
  • L'algorithme de la logique.
  • Les limites de la logique. Bogues / fonctionnalités et bogues d'angle.
  • Accès aux données d'index.

Description plus détaillée de la démo du jeu, deuxième partie :


  • Fonctionnalités utilisées de cette logique. Et le rendu rapide d'un million de particules de pixels.
  • Implémentation, quelques commentaires sur le code, description de la collision dans deux directions. Et l'interaction avec le joueur.
  • Liens vers les graphiques utilisés avec opengameart et un shader pour les ombres. Et le lien de l'article vers cyberleninka.ru

Partie 1


1. Caractéristiques clés


L'idée est une collision / physique de centaines de milliers de particules entre elles, en temps réel, où chaque particule a un identifiant unique.


Lorsque chaque particule est indexée, il est possible de contrôler tous les paramètres de n'importe quelle particule , par exemple la masse, sa santé (hp) ou les dommages, l'accélération, la décélération, les objets à rencontrer et les réactions à l'événement en fonction du type / indice de la particule, ainsi que des temporisateurs uniques pour chaque particule , et ainsi de suite si nécessaire.


Toute la logique sur GLSL est entièrement portable sur n'importe quel moteur de jeu et tout système d'exploitation prenant en charge GLES3.


Le nombre maximum de particules est égal à la taille du framebuffer (fbo, tous les pixels).


Un nombre confortable de particules (lorsqu'il y a de la place pour que les particules interagissent) est (Resolution.x*Resolution.y/2)/2 est chaque deuxième pixel en x et chaque deuxième pixel en y , c'est pourquoi la description logique le dit.


La première partie de l'article montre la logique minimale; dans la seconde, en utilisant l'exemple d'un jeu, la logique avec un grand nombre de conditions d'interaction.


2. Liens et brève description


J'ai fait trois démos sur cette logique:


1. Sur GLSL fragment-shader , sur shadertoy https://www.shadertoy.com/view/tstSz7 , voir le code BufferC dedans toute la logique. Ce code vous permet également d'afficher des centaines de milliers de particules avec leurs UV, dans une position arbitraire, sur un fragment-shader sans utiliser de particules instanciées.



2. Logique de portage sur les particules instanciées (utilisée par Godot comme moteur)



Liens Version Web , exe (win) , projet de sources particules_2D_self_collision .


Brève description: Ceci est une mauvaise démonstration sur les particules instanciées , car je fais l'augmentation maximale là où toute la carte est visible, les particules 640x360 (230k) sont toujours traitées, c'est beaucoup. Voir ci-dessous dans la description du jeu, là je l'ai bien fait, sans particules supplémentaires. (il y a une erreur d'indice de particules dans la vidéo, c'est corrigé dans le code)


3. Le jeu, à ce sujet ci-dessous dans la description du jeu. Liens Version Web , exe (win) , sources


3. L'algorithme de la logique


En bref:


La logique est similaire à celle du sable tombant, chaque pixel préserve la valeur fractionnelle de la position (décalage à l'intérieur de son pixel) et l'accélération actuelle.


La logique vérifie les pixels dans le rayon 1, que leur prochaine position veut aller à ce pixel (à cause de cette restriction, voir les restrictions ci-dessous) , aussi les pixels dans le rayon 2 pour la répulsion (collision).


L'index unique est sauvegardé en traduisant la logique en int-float, et en réduisant la taille pour la position donnée pos et la vitesse pos .


Les données sont stockées de cette manière: (à cause de ce bogue, voir restrictions)


 pixel.rgba r=[0xfffff-posx, 0xf-data] g=[0xfffff-posy, 0xf-data] b=[0xffff-velx, 0xff-data] a=[0xffff-vely, 0xff-data] 


Dans le code , numéros de ligne pour BufC https://www.shadertoy.com/view/tstSz7 , 115 transition-check, 139 collision-checks.


Ce sont de simples boucles pour prendre des valeurs adjacentes. Et la condition est, si la position est prise égale à la position du pixel actuel, alors nous déplaçons ces données vers ce pixel (en raison de cette restriction) , et la valeur de vel change en fonction des pixels voisins, le cas échéant.


C'est toute la logique des particules.


Il est préférable de placer les particules à une distance de 1 pixel les unes des autres si elles sont plus proches que 1 pixel, puis il y aura répulsion, par exemple, une carte avec un labyrinthe dans le jeu, les particules se tiennent à leur place sans se déplacer en raison de la distance de 1 pixel entre elles.


Vient ensuite le rendu (rendu), dans le cas du fragment-shader, les pixels sont pris dans un rayon de 1 pour afficher les zones d'intersection. Dans le cas de particules instanciées, un pixel est pris à l'adresse INSTANCE_ID traduit d'une vue linéaire en un tableau bidimensionnel.


4. Limitations de la logique. Bogues / fonctionnalités et bogues ANGLE


  1. La taille de pixel , BALL_SIZE dans le code, doit être dans les limites de calcul, supérieure à sqrt(2)/2 et inférieure à 1 . Plus proche de 1, moins d'espace pour marcher à l'intérieur du pixel (le pixel lui-même), moins il y a d'espace. Une telle taille est nécessaire pour que les pixels ne tombent pas entre eux, moins de 1 peut être défini lorsque vous avez de petits objets, une illusion d'objets de moins de 1 pixel (calculée) est créée.
  2. La vitesse ne peut pas dépasser 1 pixel sinon les pixels disparaîtront. Mais il est possible d'avoir une vitesse supérieure à 1 par trame, si vous effectuez plusieurs framebuffer (fbo / viewport) et traitez plusieurs étapes logiques à la fois, la vitesse de trame augmentera du nombre de fois égal au nombre de fbo supplémentaires. C'est ce que j'ai fait dans la démonstration de fruits, et en utilisant le lien vers shadertoy (bufC copié dans bufD).
  3. Limitation de la pression (comme la gravité ou une autre carte force-normale). Si plusieurs pixels voisins en prennent la position (voir l'image ci-dessus), alors un seul est enregistré, les pixels restants disparaissent. C'est facile à remarquer dans la démo sur shadertoy, placez la souris sur Force, changez la valeur de MOUSE_F en commun à 10 , et dirigez les particules vers le coin de l'écran, elles disparaîtront les unes dans les autres. Ou la même chose avec la valeur de gravité maxG en commun .
  4. Bug dans l'angle. Pour que cette logique fonctionne dans les particules GPU (instanciées), il est préférable (moins cher, plus rapide) de calculer la position, et tous les autres paramètres de particules à afficher, dans l' instance-shader . Mais Angle ne permet pas l' utilisation de plus d'une texture fbo pour un shader, donc le calcul d'une partie de la logique doit être transféré à Vertex-shader où transférer le numéro d'index du shader d'instance. C'est ce que j'ai fait dans les deux démos avec des particules GPU.
  5. Un bug sérieux dans les deux démos (sauf pour le jeu) la valeur de position sera perdue si ce n'est pas un multiple de 1/0xfffff test de bug est ici https://www.shadertoy.com/view/WdtSWS
    Plus précisément, ce n'est pas un bug, et il devrait en être ainsi, pour plus de simplicité, dans le cadre de cet algorithme, je l'ai appelé un bug.

Correction d'un bug:
Ne convertissez pas la valeur de position en flottant , car 0xff disparaîtra, 8 bits disponibles pour les données, mais la valeur 0xffff pour les données restera, ce qui peut suffire pour beaucoup de choses.
C'est ce que j'ai fait dans la démo du jeu , j'utilise uniquement 0xffff pour les données où le type de particule, le minuteur d'animation, la santé sont stockés et il y a encore de l'espace libre.


5. Accès aux données d'index


instanced-particule a son propre INSTANCE_ID , il prend un pixel de la texture du framebuffer avec la logique des particules (bufC, exemple pour shader), si là nous décompressons la particule (voir stockage de données) ID de cette particule , par cet ID nous lisons la texture avec des données pour les particules (bufB , un exemple sur un shader).


Dans l'exemple shadertoy, bufB ne stocke que la couleur de chaque particule, mais il est évident qu'il peut y avoir des données, comme la masse, l'accélération, la décélération ont écrit plus tôt, ainsi que toutes les actions logiques (par exemple, vous pouvez déplacer n'importe quelle particule à n'importe quelle position (téléport) si c'est fait) l'action logique correspondante dans le code), vous pouvez également contrôler le mouvement de n'importe quelle particule ou groupe à partir du clavier ...


Je veux dire que vous pouvez faire n'importe quoi avec chacune des particules comme s'il s'agissait de particules ordinaires dans un tableau sur le processeur, l'accès bidirectionnel à partir de la particule GPU peut changer son état, mais aussi à partir du CPU, vous pouvez changer l'état des particules par index (en utilisant des actions logiques et de la texture tampon de données).


2e partie


1. Fonctionnalités utilisées de cette logique. Et rendu rapide d'un million de particules de pixels


La taille du framebuffer (fbo / viewport) pour les particules est de 1280x720, les pièces sont situées après 1, c'est 230 mille particules actives (éléments actifs dans le labyrinthe).
Il n'y a toujours pas plus de 12 000 particules instanciées par le GPU sur l'écran.


La logique utilise:


  • La logique du lecteur est distincte de la logique des particules et ne lit que les données du tampon de particules.
  • Le joueur ralentit lorsqu'il entre en collision avec des objets.
  • Les objets de type monstre infligent des dégâts au joueur.
  • Le joueur a 2 attaques, l'une repousse tout autour, la seconde crée des particules comme une boule de feu (l'image est comme ça)
  • Le type boule de feu a sa propre masse, et le suivi bilatéral des collisions avec d'autres particules fonctionne.
  • d'autres particules telles que le plâtre et les zombies (un type de plâtre est invulnérable) sont détruites lors d'une collision avec une boule de feu
  • boule de feu s'éteint après une collision
  • niveaux physiques - les arbres et les carrés sont repoussés par le joueur, les autres particules n'interagissent pas, aucune accélération n'agit sur la boule de feu
  • les minuteries d'animation sont uniques à chaque particule

Par rapport à la démonstration de fruits, où il y a des frais généraux, dans ce jeu, le nombre de particules instanciées par le GPU n'est que de 12 mille.


Cela ressemble à ceci:



Leur nombre dépend du zoom ( zoom ) actuel de la carte, et l'augmentation est limitée à une certaine valeur, donc seuls ceux qui sont visibles à l'écran sont pris en compte.
L'écran change avec le joueur, la logique de calcul des décalages est un peu complexe, et très situationnelle, je doute qu'elle trouvera application dans un autre projet.


2. Implémentation, quelques commentaires sur le code.


Tout le code du jeu est sur le GPU.


La logique de calcul du décalage des particules dans un écran avec une augmentation de la fonction vertex dans le fichier /shaders/scene2/particle_logic2.shader est un fichier de shader de particules (vertex et fragment), pas un shader instancié, un shader instancié ne fait rien, ne passe que son index en raison de bug décrit ci-dessus.


particules par type et toute la logique de l'interaction des particules dans un fichier, il s'agit d'un fichier d'un fichier shader frame shaders / scene2 / particules_fbo_logic.shader


 // 1-2 ghost // 3-zombi // 4-18 blocks // +20 is on fire // 40 is bullet(right) 41 left 42 top 43 down 

pixel de stockage de données [pos.x, pos.y, [0xffff-vel.x, 0xff-data1],[0xffff-vel.y, 0xff-data2]]
data1 est un type, data2 est un HP ou une minuterie.


La minuterie passe sur des images dans chaque particule , la valeur maximale de la minuterie est de 255, je n'en ai pas tellement besoin, j'utilise seulement 1-16 maximum ( 0xf ), et 0xf reste inutilisé où, par exemple, vous pouvez stocker la vraie valeur HP, elle n'est pas utilisée pour moi. (c'est-à-dire, oui, j'utilise 0xff pour la minuterie , mais en fait je n'ai que moins de 16 images d'animation, et 0xf suffisant, mais je n'avais pas besoin de données supplémentaires)
En fait, 0xff utilisé que sur la minuterie de la combustion des arbres, ils se transforment en zombies après 255 images. La logique de temporisation est partiellement dans le type_hp_logic dans le shader de framebuffer de particules (lien ci-dessus).


Un exemple d'une opération de collision bidirectionnelle lorsqu'une boule de feu s'éteint au premier coup, et l'objet avec lequel elle a été frappée effectue également son action.


File shaders / scene2 / particules_fbo_logic.shader ligne 438:


 if (((real_index == 40) || (real_index == 41) || (real_index == 42) || (real_index == 43)) && (type_hp.y > 22)) { int h_id = get_id(fragCoord + vec2(float(x), float(y))); ivec2 htype_hp = unpack_type_hp(h_id); int hreal_index = htype_hp.x; if ((hreal_index != 40) && (hreal_index != 41) && (hreal_index != 42) && (hreal_index != 43)) type_hp.y = 22; } else { if (!need_upd) { int h_id = get_id(fragCoord + vec2(float(x), float(y))); ivec2 htype_hp = unpack_type_hp(h_id); int hreal_index = htype_hp.x; if (((hreal_index == 40) || (hreal_index == 41) || (hreal_index == 42) || (hreal_index == 43)) && (htype_hp.y > 22)) { need_upd = true; } } } 

real_index est un type, les types sont répertoriés ci-dessus, 40-43 est une boule de feu .
en outre type_hp.y > 22 est la valeur de la minuterie, si elle est supérieure à 22, alors la boule de feu n'a rien rencontré.
h_id = get_id(... prendre la valeur du type et HP (timer) de la particule rencontrée
hreal_index != 40... type ignoré (autre boule de feu )
type_hp.y = 22 une minuterie est réglée sur 22, c'est un indicateur que cette boule de feu est entrée en collision avec un objet.
else { if (!need_upd) variable need_upd vérifie qu'il n'y a pas de collisions répétées, puisque la fonction est en boucle, on rencontre une boule de feu .
h_id = get_id(... s'il n'y a pas encore eu de collision, nous prenons un objet de type et timer.
hreal_index == 40...htype_hp.y > 22 que l'objet de collision est une boule de feu et qu'il ne s'éteint pas.
need_upd = true qu'il est nécessaire de mettre à jour le type car il a rencontré une boule de feu .


ligne supplémentaire 481
if((need_upd)&&(real_index<24)){ real_index <24 par type inférieur à 24, il y a des arbres zombies et fantômes non brûlants, puis dans cette condition, nous mettons à jour le type en fonction du type actuel.


Ainsi, presque toute interaction d'objets peut être effectuée.


Interaction avec le joueur:


File shaders / scene2 / logic.shader line 143 function player_collision


Cette logique lit les pixels autour du joueur dans un rayon de 4x4 pixels, prend la position de chacun des pixels et la compare à la position du joueur, si un élément est trouvé, puis la vérification de type est la suivante, s'il s'agit d'un monstre, nous prenons HP du joueur.


Cela fonctionne un peu inexact et je n'ai pas voulu le réparer , cette fonction peut être rendue plus précise.


Les particules s'éloignent du joueur et l'effet de répulsion lors d'une attaque:


Un framebuffer (viewport) est utilisé pour écrire la normale des actions en cours, et les particules ( particules_fbo_logic.shader ) prennent cette texture (de la normale) à sa position et appliquent la valeur à sa vitesse et sa position. Le code entier de cette logique est littéralement juste quelques lignes, fichier force_collision.shader


Au clic du bouton gauche de la souris, des obus de boule de feu volent; leur apparence n'est pas très naturelle , ils n'ont pas corrigé et sont partis sous cette forme.


Vous pouvez soit créer une zone (forme) normale pour les particules d'apparition avec un décalage apparaissant par rapport au joueur (cela n'est pas fait).
Ou vous pouvez faire de la boule de feu un objet distinct en tant que joueur et dessiner normalement dans un tampon pour éloigner les particules de la boule de feu , c'est-à-dire par analogie avec le joueur ...
Qui a besoin de penser qu'ils le découvriront par eux-mêmes.


3. Liens vers les graphiques utilisés avec opengameart et le shadow shader


On m'a donné un lien vers un article sur cyberleninka.ru
Dans lequel la description de l'algorithme que j'ai utilisé, il y a peut-être une description plus détaillée et correcte que dans cet article, mon article.


Le shader d'ombres fonctionne très simplement, basé sur ce shader https://www.shadertoy.com/view/XsK3RR (j'ai un code modifié)
Shader construit une carte lumineuse radiale 1D



et ombrage dans le code de peinture au sol shaders / scene2 / mainImage.shader


Liens vers les graphiques utilisés , tous les graphiques du jeu depuis le site https://opengameart.org
boule de feu https://opengameart.org/content/animated-traps-and-obstacles
personnage https://opengameart.org/content/legend-of-faune
arbres et blocs https://opengameart.org/content/lolly-set-01
(et quelques autres photos avec opengameart)


Les graphiques dans le menu ont été obtenus par le shader 2D_GI, un utilitaire pour créer de tels menus:



Qui a lu jusqu'au bout - bravo :)
Si vous avez des questions, demandez, je peux compléter la description sur demande.

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


All Articles