Un autre système de particules. Post mortem

image

En septembre de cette année, le jeu mobile Titan World d'Unstoppable, le bureau de Minsk de Glu mobile, devait sortir. Le projet a été annulé juste avant la sortie mondiale. Mais les réalisations sont restées, et les plus intéressantes d'entre elles, avec l'aimable autorisation des responsables du studio Dennis Zdonov et Alex Paley, je voudrais partager avec le public.

En mars 2018, le chef d'équipe et moi avons tenu une réunion au cours de laquelle nous avons discuté de la suite des choses: le code de rendu était terminé, et il n'y avait pas de nouvelles fonctionnalités ni d'effets spéciaux dans les plans. Cela semblait être un choix logique de réécrire le système de particules à partir de zéro - selon tous les tests, il a donné les plus grands baisses de productivité, et il a rendu les concepteurs fous avec son interface (fichier de configuration de texte) et ses capacités extrêmement maigres.

Il convient de noter que la plupart du temps, l'équipe a travaillé sur le jeu en mode "version de demain", j'ai donc écrit tous les sous-systèmes, d'une part, en essayant de ne pas casser ce qui fonctionne déjà, et d'autre part, avec un cycle de développement court. En particulier, la plupart des effets dont le système standard n'était pas capable ont été effectués dans le fragment shader sans affecter le code principal.

La restriction sur le nombre de particules (des matrices de transformation pour chaque particule ont été formées sur cpu, la conclusion a été par le biais de l'installateur des ios extensibles par gl), par exemple, il était nécessaire d'écrire un shader qui «émulait» un large éventail de particules basé sur la représentation analytique de la forme des objets et composé avec l'espace retirer les fausses données dans le tampon de profondeur.

La coordonnée z du fragment a été calculée pour une particule plane, comme si nous dessinions une sphère, et le rayon de cette sphère a été modulé par le sinus du bruit de Perlin en tenant compte du temps:

r=.5+.5*sin(perlin(specialUV)+time) 

Une description complète de la reconstruction de la profondeur de la sphère peut être trouvée à Íñigo Quílez , mais j'ai utilisé un code simplifié et plus rapide. Bien sûr, c'était une approximation approximative, mais sur des formes géométriques complexes (fumée, explosions), il a donné une image assez décente.

image
Capture d'écran du gameplay. La "jupe" de fumée a été faite en une petite partie, plusieurs autres ont été laissées sur le corps principal de l'explosion. Bien sûr, cela avait l'air le plus spectaculaire «du sol», lorsque la fumée enveloppait doucement les bâtiments et les unités, cependant, les propositions de changement de position de la caméra pendant l'explosion ne sont pas entrées en production.

Énoncé du problème


Que vouliez-vous obtenir en sortant? Nous sommes plutôt sortis des limites avec lesquelles nous étions tourmentés sur l'ancien système de particules. La situation a été aggravée par le fait que le budget de trame était presque épuisé et que sur les appareils faibles (comme l'ipad air), les pipelines de pixels et de vertex étaient entièrement chargés. Par conséquent, je voulais obtenir le système le plus productif, même si je limitais un peu les fonctionnalités.

Les concepteurs ont compilé une liste de fonctionnalités et dessiné un croquis de l'interface utilisateur basé sur leur propre expérience et pratique avec unité, irréel et séquelle.

Technologie disponible


En raison de l'héritage et des restrictions imposées par le siège social, nous étions limités à opengl es 2. Par conséquent, les technologies telles que la rétroaction de transformation utilisées dans les systèmes de particules modernes n'étaient pas disponibles.

Que restait-il? Utiliser l'extraction de texture de sommet et stocker les positions / accélérations dans les textures? Une option de travail, mais la mémoire est également presque terminée, les performances d'une telle solution ne sont pas les plus optimales et le résultat n'est pas différent en termes de beauté architecturale.

À cette époque, j'avais lu de nombreux articles sur la mise en œuvre de systèmes de particules sur gpu. La grande majorité contenait un titre brillant ("des millions de particules sur le GPU mobile, avec préférence et poétesses"), cependant, la mise en œuvre se résumait à des exemples d'émetteurs / attracteurs simples, mais amusants, et en général était presque inutile pour une utilisation réelle dans le jeu.
Cet article a apporté un bénéfice maximal: l'auteur a résolu le vrai problème et n'a pas fait de «particules sphériques dans le vide». Les chiffres de référence de cet article et les résultats du profilage ont permis de gagner beaucoup de temps au stade de la conception.

Recherche d'approches


J'ai commencé par classer les problèmes résolus par le système de particules et à rechercher des cas particuliers. Il s'est avéré approximativement ce qui suit (un morceau des vrais quais du concept de la correspondance avec le chef d'équipe):
«- Tableaux de particules / mailles à mouvement cyclique. Aucune position de traitement, tout au long de l'équation du mouvement. Applications - fumée des tuyaux, vapeur sur l'eau, neige / pluie, brouillard volumétrique, balancement des arbres, utilisation partielle sur les effets non cycliques des explosions aka est possible.

- Cassettes. Formation de vb par événement, traitement uniquement sur le GPU (tirs par rayons, vols le long d'une trajectoire fixe (?) Avec trace). Peut-être que la variante avec le transfert des coordonnées début-fin aux uniformes et la construction de la bande par vertexID va décoller. avec t.z. rendre croix avec fresnel comme sur directlights + uvscroll.

- Génération de particules et traitement rapide. L'option la plus polyvalente et la plus difficile / la plus lente, voir le traitement de mouvement technique. »

En bref: il existe différents effets de particules, et certains d'entre eux peuvent être mis en œuvre plus facilement que d'autres.

Nous avons décidé de diviser la tâche en plusieurs itérations - du plus simple au plus complexe. Le prototypage a été fait sur mon moteur / éditeur sous windows / directx11 car la vitesse d'un tel développement était supérieure de plusieurs ordres de grandeur. Le projet a été compilé en quelques secondes, et les shaders ont été complètement modifiés «à la volée» et compilés en arrière-plan, affichant le résultat en temps réel et sans nécessiter de gestes supplémentaires comme appuyer sur des boutons. Quiconque a construit de grands projets avec un tas de macbook / xcode, je pense, comprendra les raisons de cette décision.

Tous les exemples de code seront tirés du prototype Windows.

image
Environnement de développement pour Windows.

Implémentation


La première étape est la sortie statique d'un réseau de particules. Rien de compliqué: démarrez le vertex bufffer, remplissez de quads (écrivez le bon uv pour chaque quad) et cousez l'id du vertex dans le uv "supplémentaire". Après cela, dans le shader, par vertex id basé sur les paramètres de l'émetteur, nous formons les positions des particules, et par uv nous restaurons les coordonnées de l'écran.

Si vertex_id est disponible en natif, vous pouvez complètement vous passer du tampon et sans uv pour restaurer les coordonnées de l'écran (comme cela a été fait dans la version Windows).

Shader:

 struct VS_INPUT { … uint v_id:SV_VertexID; … } //float index = input.uv2.x/6.0;// vertex_id   index = floor(input.v_id/6.0);// vertex_id float2 map[6]={0,0,1,0,1,1,0,0,1,1,0,1}; float2 quaduv=map[frac(input.v_id/6.0)*6]; 

Après cela, vous pouvez implémenter des scénarios simples avec une très petite quantité de code, par exemple, un mouvement cyclique avec de petites déviations convient à l'effet de neige. Cependant, notre objectif était de donner le contrôle du comportement des particules au côté des artistes, et comme vous le savez, ils savent rarement comment shader. L'option avec des préréglages de comportement et la modification des paramètres via les curseurs n'a pas non plus séduit - changement de shaders ou ramification à l'intérieur, multiplication des options prédéfinies, manque de contrôle total.

La tâche suivante consistait à implémenter un fondu entrant / sortant pour un tel système. Les particules ne doivent pas apparaître de nulle part et disparaître dans nulle part. Dans l'implémentation classique d'un système de particules, nous traitons le tampon par programmation en utilisant cpu, en créant de nouvelles particules et en supprimant les anciennes. En fait, pour obtenir de bonnes performances, vous devez écrire un gestionnaire de mémoire intelligent. Mais que se passe-t-il si vous ne dessinez simplement pas les particules "mortes"?

Supposons (pour commencer) que l'intervalle de temps de l'émission de particules et la durée de vie d'une particule soit une constante au sein d'un même émetteur.

image
Ensuite, nous pouvons présenter de manière spéculative notre tampon (qui ne contient que le vertex id) comme circulaire et déterminer sa taille maximale comme suit:

 pCount = round (prtPerSec * LifeTime / 60.0); pCountT = floor (prtPerSec * EmissionEndTime / 60.0); pCount=min (pCount, pCountT); 

et dans le shader, calculer le temps en fonction de l'indice et du temps (temps écoulé depuis le début de l'effet)

 pTime=time-index/prtPerSec; 

Si l'émetteur est dans une phase cyclique (toutes les particules sont émises et meurent maintenant et naissent de manière synchrone), nous faisons une fracturation à partir du moment de la particule et obtenons ainsi une boucle.

Nous n'avons pas besoin de dessiner des particules avec pTime inférieur à zéro - elles ne sont pas encore nées. Il en va de même pour les particules dans lesquelles la somme de la durée de vie et du temps actuel dépasse le temps de fin d'émission. Dans les deux cas, nous ne dessinerons rien en annulant la taille des particules et / ou en la laissant derrière l'écran. Cette approche donnera une petite surcharge dans les phases de fadein / fadeout, tout en maintenant des performances maximales dans la phase de maintien.

L'algorithme peut être légèrement amélioré en envoyant uniquement la partie du tampon de sommet qui contient des particules vivantes pour le rendu. Du fait que l'émission se produit séquentiellement, les particules vivantes seront segmentées au plus une fois, c'est-à-dire deux appels sont nécessaires.

Maintenant, connaissant l'heure actuelle de chaque particule, vous pouvez définir la vitesse, l'accélération (et, en général, tout autre paramètre) pour écrire l'équation du mouvement, ce qui entraîne les coordonnées dans l'espace mondial.

En utilisant restauré à partir de vertex_id uv, nous aurons déjà quatre points (plus précisément, nous déplacerons chacun des points quad dans la direction dont nous avons besoin), sur lesquels le vertex shader, après avoir terminé la projection, achèvera son travail.

 p.xy+=(quaduv-.5); 

Avec le bonus gratuit, nous avons eu l'occasion non seulement de mettre l'émetteur en pause, mais aussi de rembobiner le temps d'avant en arrière avec précision sur l'image. Cette fonctionnalité s'est avérée très utile dans la conception d'effets complexes.

Nous augmentons la fonctionnalité


L'itération suivante du développement a été la solution au problème d'un émetteur en mouvement. Notre système particulier ne savait rien de sa position, et lorsque l'émetteur se déplaçait, tout l'effet se déplaçait de manière synchrone derrière lui. Pour la fumée du tuyau d'échappement et les effets similaires, cela semblait plus qu'étrange.

L'idée était d'enregistrer la position de l'émetteur dans un tampon de vertex à la naissance d'une nouvelle particule. Étant donné que le nombre de ces particules est faible, le surdébit aurait dû être minime.

Un collègue a suggéré que lors du développement de sa propre interface utilisateur, il n'utilisait map / unmap qu'une partie du tampon vertex et était très satisfait des performances de cette solution. J'ai fait des tests, et il s'est avéré que cette approche fonctionne vraiment bien sur les plates-formes de bureau et mobiles.

La difficulté est apparue avec la synchronisation de l'heure sur cpu et gpu. Il était nécessaire de s'assurer que la mise à jour du tampon était effectuée exactement lorsque la «nouvelle» particule bouclée était dans sa position de départ. Autrement dit, par rapport au tampon en anneau, il est nécessaire de synchroniser les limites de la région de mise à jour avec le temps de fonctionnement de l'émetteur.

J'ai transféré le code hlsl en C ++, pour le test j'ai écrit l'émetteur se déplaçant autour de Lissajous, et tout cela a soudainement fonctionné. Cependant, de temps en temps, le système «crachait» sur une ou plusieurs particules, les tirant dans une direction arbitraire, ne les retirant pas à temps ou n'en créant pas de nouvelles dans des endroits arbitraires.

Le problème a été résolu en vérifiant la précision du calcul de l'heure dans le moteur et en vérifiant simultanément le delta de temps lors de l'enregistrement de la nouvelle position de l'émetteur - de sorte que la section tampon entière qui n'était pas affectée par l'itération précédente a été mise à jour. Il était également nécessaire que le système fonctionne dans les conditions d'une désynchronisation forcée - un retrait soudain de fps ne devrait pas casser l'effet, d'autant plus que pour différents appareils, notre jeu a enregistré des fps différents en fonction de la performance - 60/30/20.

Le code de la méthode a beaucoup augmenté (le tampon en anneau est difficile à traiter avec élégance), cependant, après avoir pris en compte toutes les conditions, le système a fonctionné correctement et de manière stable.

A cette époque, le partenaire avait déjà fait le «poisson» de l'éditeur, suffisant pour tester le système, et écrit les modèles / api pour intégrer le système dans notre moteur.

J'ai porté tout le code sur ios / opengl, intégré et finalement fait de vrais tests d'effets sur un vrai appareil. Il est devenu clair que le système fonctionne non seulement, mais est également adapté à la production. Il restait à terminer l'éditeur d'interface utilisateur et à peaufiner le code à l'état «ce n'est pas effrayant de le donner pour le sortir demain».

Nous étions déjà prêts à écrire un gestionnaire de mémoire afin de ne pas allouer / détruire un tampon (qui a finalement stocké vertex_id, uv, position et initial particule vector) pour chaque nouvel effet avec un émetteur dynamique, comme une autre idée m'est venue à l'esprit.

Le fait de l'existence du vertex buffer dans ce système me hantait. Il regardait clairement dans son archaïsme "l'héritage des âges sombres du convoyeur fixe". Lorsque je faisais des effets de test sur un prototype Windows, je pensais que le mouvement de l'émetteur était toujours fluide et toujours beaucoup plus lent que le mouvement des particules. De plus, avec un grand nombre de particules, la mise à jour de la position conduit au fait que des centaines de particules enregistrent les mêmes données. La solution s'est avérée simple: nous introduisons un réseau fixe dans lequel «l'historique» de la position de l'émetteur, normalisé par la durée de vie de la particule, va tomber. Et sur gpu, nous interpolerons les données. Après cela, le besoin de tampons dynamiques a disparu dans la version ios / gles2 (seule la statique générale restait pour implémenter vertex_id), et dans les versions windows / dx11, les tampons ont complètement disparu en raison du vertex_id natif et de la capacité de l'api d3d à accepter null au lieu de se lier au tampon vertex.

Ainsi, la version gagnant du système, selon les normes modernes, ne consomme pas du tout de mémoire, peu importe le nombre de particules que nous voulons afficher. Seul un petit tampon constant avec paramètres, un tampon de positions / bases (60 paires de vecteurs se sont avérés être suffisants, avec une marge, dans tous les cas), et, si nécessaire, la texture. Les mesures de performances montrent une vitesse proche des tests synthétiques.

De plus, la «queue» d'effets comme des étincelles commençait à paraître beaucoup plus naturelle, puisque l'interpolation permettait de supprimer la discrétisation par images et donc l'émetteur changeait de position en douceur, comme si les appels de dessin étaient effectués à une fréquence de centaines de hertz.

CARACTÉRISTIQUES


En plus des fonctionnalités de base du vol des particules (vitesse, accélération, gravité, résistance du milieu), nous avions besoin d’une certaine quantité de «graisse» fonctionnelle.
En conséquence, le flou de mouvement (étirement d'une particule le long d'un vecteur de mouvement), l'orientation des particules à travers le vecteur de mouvement (cela permet, par exemple, de créer une sphère de particules), le redimensionnement de la particule en fonction de l'heure actuelle de sa vie, et des dizaines d'autres petites choses ont été mises en œuvre.

La complexité est apparue avec les champs vectoriels: le système ne stockant pas son état (position, accélération, etc.) pour chaque particule, mais les calculant à chaque fois par le biais de l'équation du mouvement, un certain nombre d'effets (comme le mouvement de la mousse lors de l'agitation du café) étaient en principe impossibles. Cependant, une simple modulation de la vitesse et de l'accélération par le bruit du perlin a donné des résultats assez modernes. Le calcul du bruit en temps réel pour tant de particules s'est révélé trop cher (même avec une limite de cinq octaves), donc une texture a été générée à partir de laquelle le vertex shader échantillonnerait. Pour améliorer l'effet d'un faux champ vectoriel, un petit décalage des coordonnées de l'échantillon a été ajouté en fonction de l'heure actuelle de l'émetteur.

image
Le test de fumée de cigarette fonctionne en répartissant la vitesse et l'accélération initiales sur le bruit perlin.

Convoyeur de pixels


Initialement, nous avions seulement prévu de changer la couleur / transparence de la particule en fonction de son temps. J'ai ajouté plusieurs algorithmes au pixel shader.

Rotation des couleurs de la texture - simplifiée, sin (couleur + temps). Permet dans une certaine mesure d'imiter l'effet de permutation d'AfterEffects.

Faux éclairage - modulation de la couleur d'une particule par un gradient en coordonnées universelles, quel que soit l'angle de rotation de la particule.

Évolution des frontières - lorsqu'une particule se déplace dans l'espace, ses frontières (canal alpha) sont modulées par une combinaison de projecteurs et de bruit perlin, ce qui donne leur dynamique d'écoulement, très similaire aux nuages, à la fumée et à d'autres effets de fluide.

Pseudo-code du shader:

 b=perlin(uv);// , uv      a=saturate(1-length(input.uv.xy-.5)*2);//     a-=abs(ab);//””,   

Dans une version légèrement compliquée, ce shader pouvait tracer des frontières avec une douceur arbitraire et un éclairage de contour, ce qui ajoutait des effets «explosifs» au réalisme.

image
Les premières expériences avec l'évolution des frontières.

Et ensuite?


Malgré l'éditeur, déjà prêt à travailler et intégré dans le moteur, les concepteurs n'ont pas eu le temps d'y faire un seul effet - le projet était clos. Néanmoins, il n'y a aucun obstacle à l'utilisation de ces pratiques ailleurs - par exemple, pour effectuer un travail sur la version de démonstration.

D'un point de vue technologique, il y a aussi de la place pour bouger - maintenant, par exemple, plusieurs effets de destruction d'objets filaires sont en action:

image

La question du tri des particules pour le mélange alpha reste ouverte jusqu'à présent: puisque tout est considéré analytiquement dans le shader, il n'y a en fait aucune donnée d'entrée pour le tri. Mais il y a un large champ d'expérimentation!

Pendant le développement de Titan World, de nombreuses astuces ont été appliquées dans la partie graphique du jeu, mais plus à ce sujet la prochaine fois.

PS Vous pouvez creuser dans le moteur alpha source ici . Les exemples sont dans le dossier release / samples, les touches de contrôle principales sont l'espace, alt | control + mouse. Les shaders se trouvent directement dans les fichiers fxp, leur code est disponible via la fenêtre de l'éditeur.

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


All Articles