GPU Bound. Deuxième partie Forêt sans fin



Dans presque tous les jeux, il est nécessaire de remplir les niveaux de jeu avec des objets qui créent la richesse visuelle, la beauté et la variabilité du monde virtuel. Prenez n'importe quel jeu en monde ouvert. Là, les arbres, l'herbe, la terre et l'eau sont les principaux «espaces réservés» de l'image. Aujourd'hui, il y aura très peu de GPGPU, mais je vais essayer de vous dire comment dessiner beaucoup d'arbres et de pierres dans le cadre quand vous ne pouvez pas, mais que vous le voulez vraiment.

Il convient de noter immédiatement que nous avons un petit studio indépendant, et nous n'avons souvent pas les ressources pour dessiner et modéliser chaque petite chose. D'où la nécessité pour les différents sous-systèmes d'être une «superstructure» sur la fonctionnalité prête à l'emploi du moteur. C'était donc dans le premier article du cycle sur les animations (là, nous avons utilisé et accéléré le système d'animation Unity terminé), donc ce sera ici. Cela simplifie considérablement l'introduction de nouvelles fonctionnalités dans le jeu (moins à apprendre, moins de bugs, etc.).

Donc, la tâche: vous devez dessiner beaucoup de forêt. Le jeu, nous avons une stratégie en temps réel (RTS) avec de grands niveaux (30x30 km), et cela définit les exigences de base pour le système de rendu:

  • Avec l'aide de la minicarte, nous pouvons transférer instantanément à n'importe quel point du niveau. Et les données sur les objets pour la nouvelle position devraient être prêtes. Nous ne pouvons pas compter sur le chargement des ressources après un certain temps dans les jeux FPS ou TPS.
  • Les objets à de si grands niveaux ont besoin d'un très grand nombre. Des centaines de milliers, sinon des millions.
  • Encore une fois, les niveaux élevés rendent très longue et difficile la configuration manuelle de "forêts". La génération procédurale de forêt, de pierres et d'arbustes est nécessaire, mais avec la possibilité d'un ajustement et d'un agencement manuels aux endroits clés du niveau de jeu.

Comment résoudre ce problème? Un tel nombre d'objets arrangés ordinaires de l'unité ne tirera toujours pas. Nous mourrons dans l'abattage et la mise en lots. Le rendu est possible en utilisant l'instanciation. Il est nécessaire d'écrire un système de contrôle. Les arbres doivent être modélisés. Un système d'animation d'arbre doit être fait. Ooh Je le veux magnifiquement et immédiatement. Il y a SpeedTree, mais il n'y a pas d'api pour les animations, la vue de dessus des panneaux d'affichage est terrible, car il n'y a pas de "panneau horizontal" et la documentation est pauvre. Mais quand cela nous a-t-il arrêtés? Nous optimiserons le rendu SpeedTree.

Rendu


Voyons voir si tout va si mal avec des objets Speedtree ordinaires:



Voici environ 2 000 arbres sur scène. Tout est en ordre avec le rendu, instanciant là les arbres en lots, mais avec le CPU tout va mal. La moitié du temps de rendu de la caméra refroidit. Et nous avons besoin de centaines de milliers. Nous refusons définitivement les GameObjects, mais maintenant nous devons découvrir la structure du modèle SpeedTree, le mécanisme de commutation LOD et tout faire avec les poignées.

L'arbre SpeedTree se compose de plusieurs niveaux de détail (généralement 4), dont le dernier est un panneau d'affichage, et tout le reste est une géométrie de divers degrés de détail. Chacun d'eux se compose de plusieurs sabmesh, avec son propre matériau:


Ce n'est pas la spécificité de SpeedTree. Toute structure peut avoir une telle structure. La commutation LOD est implémentée dans deux modes disponibles:

  1. Fondu croisé:

  2. Arbre de vitesse:


CrossFade (en termes de shaders Unity, il est défini par le préprocesseur LOD_FADE_CROSSFADE define) est la principale méthode de commutation LOD pour tous les objets de scène avec plusieurs niveaux de détail. Il consiste en ce que lorsque le LOD est modifié, le maillage qui doit disparaître ne disparaît pas simplement (le saut de qualité du modèle sera clairement visible), mais «se dissout» sur l'écran à l'aide du tramage . Un effet simple, et évite l'utilisation d'une véritable transparence (mélange alpha). Le modèle qui doit apparaître exactement de la même manière "apparaît" à l'écran.

SpeedTree (LOD_FADE_PERCENTAGE) est spécialement conçu pour les arbres. En plus des coordonnées principales, les coordonnées supplémentaires de la position des sommets juniors par rapport au niveau LOD actuel sont enregistrées dans la géométrie des feuilles, des branches et du tronc. Le degré de transition d'un niveau à un autre est la valeur de poids pour l'interpolation linéaire de ces deux positions. Le déplacement vers / depuis le panneau d'affichage se fait à l'aide de la méthode CrossFade.

En principe, c'est tout ce que vous devez savoir pour mettre en œuvre votre propre système de commutation LOD. Le rendu lui-même est simple. Nous parcourons tous les types d'arbres, à travers tous les LOD et à travers tous les sabmesh de chaque LOD. Nous installons le matériel approprié et dessinons toutes les instances de cet objet d'un seul coup en utilisant l'instanciation. Ainsi, le nombre de DrawCalls est égal au nombre d'objets uniques dans la scène. Comment savons-nous quoi dessiner? Cela nous aidera

Générateur de forêt


L'atterrissage lui-même est simple et sans prétention. Pour chaque type d'arbre, nous divisons le monde en quads afin que chaque arbre corresponde à un arbre. Nous passons en revue tous les quads et vérifions le masque du formulaire:



à un point donné du niveau, est-il possible de planter un arbre ici? Le masque, aux endroits "boisés", est dessiné par le level designer. Au début, tout était sur le CPU et le C #. Le générateur fonctionnait lentement et la taille des niveaux augmentait de sorte que l'attente de la régénération pendant plusieurs dizaines de minutes devenait stressante. Il a été décidé de transférer le générateur vers le GPU et le shader de calcul. Ici aussi, tout est simple. Nous avons besoin de la carte de la hauteur du terrain, du masque de plantation d'arbres et d'AppendStructuredBuffer, où nous ajoutons les arbres générés (position et ID, ce sont toutes les données).

Disposé par des arbres à main à des points clés, un script spécial s'inscrit dans des tableaux communs et supprime l'objet d'origine de la scène.

Élimination et commutation LOD


Connaître la position et le type d'arbre ne suffit pas pour faire un rendu efficace. Il est nécessaire de déterminer à chaque image quels objets sont visibles et quel LOD (compte tenu de la logique de transition) envoyer au rendu.

Un shader de calcul spécial fera également cela. Pour chaque objet, Frustum Culling est d'abord effectué:


Si l'objet est visible, la logique de commutation LOD est exécutée. En fonction de la taille à l'écran, nous déterminons le niveau de LOD souhaité. Si le mode CrossFade est défini pour le LOD du groupe, nous incrémentons le temps de transition pour le tramage. Si SpeedTree Percentage, alors nous considérons la valeur de transition normalisée entre les LOD.

Les API graphiques modernes ont de merveilleuses fonctions qui permettent de transmettre des informations de soumission de dessin à l'appel de dessin dans le tampon de calcul (par exemple, ID3D11DeviceContext :: DrawIndexedInstancedIndirect pour D3D11). Cela signifie que vous pouvez également remplir ce tampon de calcul sur le GPU. Ainsi, il s'avère que le système est entièrement indépendant du processeur (enfin, il suffit d'appeler Graphics.DrawMeshInstancedIndirect). Dans notre cas, il suffit d'enregistrer le nombre d'instances de chaque sabmesh. Le reste des informations (le nombre d'indices dans le maillage et les décalages) est statique.

Le tampon de calcul, avec des arguments pour l'appel de dessin, est divisé en sections, chacune étant chargée d'appeler le rendu de son sous-maillage. Dans le shader de calcul du maillage à dessiner dans l'image actuelle, incrémentez la valeur InstanceCount correspondante.

Voici à quoi cela ressemble dans le rendu:


L'abattage d'occlusion GPU est la prochaine étape évidente, mais pour RTS avec un tel appareil photo, et pas de très grandes collines, les gains ne sont pas si évidents (et ici c'est pour ceux qui sont intéressés). Je ne l'ai pas encore fait.

Pour que tout soit dessiné correctement, vous devez modifier légèrement les shaders SpeedTree pour prendre la position et les valeurs des transitions entre les LOD des tampons de calcul correspondants.

Nous dessinons maintenant de beaux arbres statiques. Et les arbres SpeedTree sont influencés de manière réaliste par le vent, les animant. Toute la logique de ces animations se trouve dans le fichier SpeedTreeWind.cginc, mais il n'y a pas de documentation ou d'accès aux paramètres internes depuis Unity.

CBUFFER_START(SpeedTreeWind) float4 _ST_WindVector; float4 _ST_WindGlobal; float4 _ST_WindBranch; float4 _ST_WindBranchTwitch; float4 _ST_WindBranchWhip; float4 _ST_WindBranchAnchor; float4 _ST_WindBranchAdherences; float4 _ST_WindTurbulences; float4 _ST_WindLeaf1Ripple; float4 _ST_WindLeaf1Tumble; float4 _ST_WindLeaf1Twitch; float4 _ST_WindLeaf2Ripple; float4 _ST_WindLeaf2Tumble; float4 _ST_WindLeaf2Twitch; float4 _ST_WindFrondRipple; float4 _ST_WindAnimation; CBUFFER_END 

Comment pourrions-nous les choisir? Pour ce faire, pour chaque type d'arbre, nous rendrons l'objet SpeedTree d'origine quelque part dans un endroit invisible (ou plutôt, visible dans Unity, mais pas visible dans la caméra, sinon les paramètres ne seront pas mis à jour). Ceci peut être réalisé en augmentant considérablement le cadre de sélection et en plaçant l'objet derrière la caméra). Chaque cadre est supprimé du jeu de valeurs souhaité à l'aide de material.GetVector (...).

Ainsi, les arbres flottent au vent, mais la vue de dessus des panneaux d'affichage est déprimante:


Avec l'option shader BILLBOARD_FACE_CAMERA_POS encore pire:


Nous avons besoin de panneaux d'affichage horizontaux (de haut en bas). Il s'agit d'une fonctionnalité SpeedTree standard depuis l'époque de King Pea, mais à en juger par les forums, elle n'est toujours pas implémentée dans Unity. Message du forum officiel de SpeedTree: "L'intégration Unity n'a jamais utilisé le panneau d'affichage horizontal." Nous allons attacher nos mains. La géométrie elle-même est facile à réaliser. Comment trouver les coordonnées UV d'un sprite dans un atlas pour elle?


Nous obtenons l'ancien SDK SpeedTreeRT, et nous trouvons la structure dans la documentation:

 struct SBillboard { bool m_bIsActive; const float* m_pTexCoords; const float* m_pCoords; float m_fAlphaTestValue; }; 

"M_pTexCoords pointe vers un ensemble de 4 (s, t) coordonnées de texture qui définissent les images utilisées sur le panneau d'affichage. m_pTexCoords contient 8 entrées. », indique-t-il dans une langue étrangère. Eh bien, nous allons chercher une séquence de 4 valeurs à virgule flottante dans un fichier spm binaire, chacune se situant dans la plage [0..1]. Par la méthode de piquer scientifique, nous découvrons que la séquence souhaitée est devant un bloc de 12 flotteurs avec des signes correspondant au motif:

 float signs[] = { -1, 1, -1, 1, 1, -1, 1, 1, 1, -1, 1, 1 }; 

Nous écrivons un petit utilitaire de console sur les pros, qui itère sur tous les fichiers spm et recherche des coordonnées uv pour les panneaux d'affichage horizontaux. La sortie est une telle étiquette CSV:

 Azalea_Desktop.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Azalea_Desktop_Flowers_1.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Azalea_Desktop_Flowers_2.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Leaf_Map_Maker_Desktop_1_Modeler_Use_Only.spm: Pattern not found! Leaf_Map_Maker_Desktop_2_Modeler_Use_Only.spm: Pattern not found! BarrelCactus_Cluster_Desktop_1.spm: 0, 0.592376, 0.407624, 0.592376, 0.407624, 0.184752, 0, 0.184752, BarrelCactus_Cluster_Desktop_2.spm: 0, 1, 0.499988, 1, 0.499988, 0.500012, 0, 0.500012, BarrelCactus_Desktop_1.spm: 0, 0.2208, 0.220748, 0.2208, 0.220748, 5.29885e-05, 0, 5.29885e-05, BarrelCactus_Desktop_2.spm: 0, 1, 0.301392, 1, 0.301392, 0.698608, 0, 0.698608, 

Pour affecter des coordonnées de texture à la géométrie du panneau d'affichage horizontal, nous trouvons l'enregistrement souhaité et l'analysons.

Maintenant, c'est comme ça:


Toujours pas très. En utilisant le seuil de test alpha, nous atténuerons le panneau d'affichage vertical, dans les enregistrements de l'angle à la caméra:



Résumé

Profileur affichant des statistiques dynamiques (combien de choses sont rendues) et statiques (combien d'objets et leurs paramètres sont sur la scène):


Eh bien, la belle vidéo finale (la seconde moitié montre la commutation des niveaux de qualité):


Ce que nous avons finalement:

  • Le système est entièrement indépendant du CPU.
  • Ça marche vite.
  • Il utilise des actifs SpeedTree prêts à l'emploi, que vous pouvez acheter sur Internet.
  • Bien sûr, je me suis fait des amis avec n'importe quel LODGroup, pas seulement avec SpeedTree. Tant de galets sont désormais également possibles.

Parmi les lacunes, on peut noter le manque d'élimination des occlusions et des panneaux d'affichage encore peu expressifs.

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


All Articles