Comment j'ai fait des ombres 2D dans Unity

Qu'est-ce qui vient à l'esprit d'un développeur de jeux indépendants lorsqu'il est confronté à la nécessité d'ajouter une fonctionnalité qu'il n'a aucune idée de la mise en œuvre? Bien sûr, il va chercher des traces de ceux qui ont déjà parcouru ce chemin et qui ont pris la peine d'écrire leur expérience. Je l'ai donc fait il y a quelque temps, en commençant à créer des ombres dans mon jeu. Trouver la bonne information - sous forme d'articles, de leçons et de guides - n'a pas été difficile. Cependant, à ma grande surprise, j'ai trouvé qu'aucune des solutions décrites ne me convenait tout simplement. Par conséquent, après avoir réalisé le mien, j'ai décidé d'en parler au monde.

Il convient d'avertir à l'avance que ce texte ne prétend pas être une sorte de guide ultimatum ou de master class. La méthode que j'ai utilisée n'est peut-être pas universelle, loin d'être la plus efficace et ne couvre pas entièrement la tâche de créer des ombres bidimensionnelles. C'est plutôt une histoire sur les astuces auxquelles un développeur inexpérimenté a dû faire face pour obtenir un résultat satisfaisant à ses exigences.

Le résultat lui-même est devant vous:



Et les détails du chemin vers sa réalisation vous attendent sous la coupe.

Un peu sur le jeu lui-même
Dwarfinator est un jeu de tir de défense / défilement latéral bidimensionnel développé avec un œil sur les segments mobile et de bureau. Le gameplay consiste en la destruction systématique des vagues ennemies dans deux modes alternatifs - défense et poursuite. La progression d'un joueur implique de pomper un «réservoir» en améliorant et en remplaçant divers éléments, tels que les armes, les moteurs et les roues, ainsi qu'en élevant le niveau et en apprenant des compétences actives et passives. La progression de l'environnement implique une augmentation constante du nombre de mobs dans la vague, l'ajout de nouveaux types d'ennemis à la vague au fur et à mesure qu'ils progressent à travers l'emplacement et le changement successif de plusieurs emplacements, chacun ayant son propre ensemble d'adversaires.

Énoncé du problème


Donc, au moment de la décision d'ajouter des ombres au jeu, j'avais:

  • emplacement sous la forme de deux sprites, l'un pour l'affichage derrière les foules et autres entités, le second pour l'affichage devant eux;



  • des foules et des objets destructibles statiques, constamment animés et constitués de sprites séparés en quantité de quelques à quelques dizaines;



  • des obus, propres et ennemis, représentés dans la plupart des cas soit par un sprite, soit par un système de particules, dans ce dernier cas aucune ombre n'était requise;



  • un char constitué de plusieurs pièces assemblées selon le même schéma que les foules;



  • murs avec plusieurs états fixes, qui, encore une fois, sont un ensemble de sprites séparés.



Pour tout cela, les ombres les plus simples étaient nécessaires, répétant les contours de l'objet et projetées à partir d'une seule source de lumière fixe.

Dans le même temps, il faut avoir une attitude attentive à la productivité. En raison des spécificités du genre et des particularités de sa mise en œuvre, la plupart des objets projetant des ombres sont localisés directement sur l'écran à tout moment. Et leur nombre total peut être supérieur à cent, si nous parlons d'entités de jeu, et quelques milliers, si nous parlons de sprites individuels.

Implémentation


En fait, le principal problème s'est avéré être que Dwarfinator, en gros, est un jeu 2.5D. La grande majorité des objets existent dans un espace à deux dimensions avec les axes X et Y, et l'axe Z est extrêmement rarement utilisé. Visuellement, et en partie gameplay, l'axe Y est utilisé pour afficher à la fois la hauteur et la profondeur, se divisant de la même manière dans les axes virtuels Y et Z. Il n'était pas possible d'utiliser des outils Unity standard dans une telle situation pour créer des ombres.

Mais en fait, je n'avais pas besoin d'un éclairage honnête, c'était suffisant pour pouvoir créer manuellement une ombre pour chaque objet. Par conséquent, la chose la plus simple qui m'est venue à l'esprit était de simplement en placer une copie derrière chaque entité, tournée dans un espace tridimensionnel afin de simuler un emplacement sur la surface. Tous les sprites de cette pseudo-ombre ont été mis à noir, tandis que la structure hiérarchique du propriétaire de l'ombre a été préservée, ce qui lui a permis d'être animée en synchronisation avec le propriétaire par le même animateur.

Une telle animation synchrone ressemblait à ceci:



Cependant, l'ombre exigeait de la transparence. La solution la plus simple était de la définir pour chaque sprite d'ombre. Mais une telle mise en œuvre ne semblait pas satisfaisante - les sprites se chevauchaient, formant des zones moins transparentes sur le site de superposition.

La capture d'écran ci-dessous montre à quoi ressemble l'ombre de plusieurs segments translucides. Les paramètres de distorsion de l'ombre utilisés sont également visibles: rotation le long de l'axe X de -50 degrés, rotation le long de l'axe Y de -140 degrés et l'échelle le long de l'axe X, augmentée de 1,3 fois par rapport à l'objet parent.



Il est devenu évident que la transparence devait être imposée à l'ombre en tant qu'objet solide. La première expérience sur ce sujet a été suspendue à l'ombre de la caméra, rendant cette ombre dans RenderTexture, qui a ensuite été utilisée comme matériau attaché au parent de l'ombre du plan. Il pouvait déjà établir la transparence sans aucun problème. Les ombres elles-mêmes étaient en dehors du cadre pour éviter le chevauchement des zones de capture de la caméra. L'approche a fonctionné, mais il s'est avéré que déjà quelques dizaines d'ombres causaient de sérieux problèmes de performances, principalement en raison du nombre de caméras sur la scène. De plus, un certain nombre d'animations supposaient un mouvement significatif des sprites de mob individuels dans le cadre de son objet racine, en raison duquel une zone de caméra devrait être située qui dépasserait considérablement la taille de l'image réelle à un moment donné.

La solution a été trouvée rapidement - si vous ne pouvez pas dessiner chaque ombre avec une caméra séparée - pourquoi ne pas dessiner toutes les ombres avec une seule caméra? Tout ce qui devait être fait était de placer une zone séparée de la scène sous l'ombre, légèrement plus élevée que le champ de vision de la caméra principale, de diriger une caméra supplémentaire vers cette zone et d'afficher sa sortie entre l'emplacement et d'autres entités.

Ci-dessous, vous pouvez voir un exemple de la sortie de cette caméra:



La productivité d'une telle implémentation a beaucoup moins souffert, donc la solution a été considérée comme fonctionnelle et appliquée à tous les mobs, objets statiques et obus. Cela a été suivi par l'emplacement du sprite. Il était impossible d'utiliser un sprite sur tous les objets, car il a été implémenté précédemment. L'utilisation d'une copie d'un objet comme ombre fonctionne uniquement tant que l'objet est complètement plat. Même lors de la création d'ombres pour les foules, il était à noter que les points de contact avec la surface espacée le long de la troisième coordonnée violaient l'exactitude de l'ombre par rapport à ces points.

La capture d'écran suivante montre un exemple d'une telle violation. Le talon de la foule est considéré comme le point de contact avec la surface, mais les ombres des pieds sont déjà au-delà des pieds eux-mêmes.



Et si dans le cas des pattes de l'ogre vous pouvez encore légèrement changer la position de l'ombre et masquer le problème, alors pour plusieurs dizaines de troncs d'arbres, il n'y a aucune chance. Tous les objets de localisation qui étaient censés projeter une ombre doivent être séparés en GameObject. C'est exactement ce que j'ai fait en plaçant des copies des objets destructibles correspondants sur le préfabriqué d'emplacement et en désactivant les scripts qui ne sont pas utilisés dans cette position. En même temps, grâce à cela, il est devenu possible de les inclure dans le tri général des objets de la scène, et les obus volant à l'extérieur de l'emplacement n'étaient plus dessinés strictement au-dessus de tous les objets, mais volaient entre eux. De plus, il est devenu possible de faire animer les objets eux-mêmes.

Mais alors un nouveau problème m'attendait. Avec des ombres et des dizaines de nouveaux objets, le nombre maximum de GameObjects simultanément sur la scène, et avec eux les composants Animator et SpriteRenderer, a plus que doublé. Lorsque j'ai libéré toute la vague de mobs à l'endroit, qui représentait environ 150 pièces, Profiler m'a montré un reproche d'environ 40 ms, qui ne sont partis que pour le rendu et l'animation, et la fréquence d'images variait généralement autour de 10. J'ai désespérément optimisé mes propres scripts, luttant pour chaque milliseconde, mais cela ne suffisait pas.

À la recherche d'outils d'optimisation supplémentaires, je suis tombé sur la vaste documentation et les guides pour le traitement par lots dynamique.

Un peu plus sur le traitement par lots
En bref, le traitement par lots est un mécanisme pour minimiser le nombre d'appels de tirage, et avec lui le temps passé au moment du rendu du cadre sur l'interaction entre le CPU et le GPU. Lorsqu'ils sont utilisés au lieu d'envoyer chaque élément individuellement pour le rendu, des éléments similaires sont regroupés et dessinés ensemble à la fois. Dans le cas d'Unity, le moteur lui-même essaie d'utiliser au maximum ce mécanisme et presque aucune action supplémentaire n'est requise de la part du développeur.

Frame Debugger a montré que j'avais, au mieux, les détails de chaque objet ou mob séparément. Ayant créé des sprites pour le premier et le deuxième de l'atlas, j'ai réalisé l'ombre avec seulement quelques appels de tirage, mais les propriétaires de ces ombres ont obstinément refusé de se battre.

Des expériences sur une scène distincte ont montré que le traitement par lots dynamique se casse lorsque les objets ont un composant SortingGroup, que j'ai utilisé pour trier l'affichage des entités à l'écran. Il était possible de s'en passer, cependant, en théorie, définir les valeurs de tri pour chaque sprite et système de particules dans un objet séparément pourrait s'avérer encore plus cher que l'absence de traitement par lots.

Mais quelque chose me hantait. L'objet fantôme, étant un descendant de l'objet hôte dans la scène réelle, appartenait techniquement au même groupe de tri, cependant, il n'y avait aucun problème avec l'observation dynamique des objets fantômes. La seule différence était que les objets hôtes étaient dessinés directement sur l'écran par la caméra principale, et les objets ombres étaient d'abord rendus dans RenderTexture.

C'était le hic. La raison exacte de ce comportement est inconnue d'Internet, mais lors du rendu des images de la caméra dans RenderTexture, SortingGroup n'a plus interrompu le traitement par lots. La décision semblait très étrange, illogique et en général la plus béquille. Mais en mettant en œuvre le rendu des entités en utilisant la même méthode que le rendu des ombres, et ayant ainsi obtenu, en plus de la couche d'ombre, une couche d'entité, j'ai déjà atteint des valeurs de performances tout à fait acceptables.

La capture d'écran ci-dessous montre un exemple de rendu d'une couche d'entité.



Donc, en général, le rendu d'une certaine entité dans la coordonnée Y ressemble à ceci:

  1. L'entité est placée à Y-20;
  2. Une entité est rendue par une caméra observant cette coordonnée dans une RenderTexture pour les entités;
  3. L'ombre de l'entité est placée à Y + 20;
  4. L'ombre d'une entité est dessinée par une caméra qui observe cette coordonnée dans une texture de rendu pour les ombres;
  5. La caméra principale dessine l'image-objet de l'emplacement principal sur l'écran - le seul élément qui est actuellement rendu directement à l'écran;
  6. La caméra principale dessine un plan sur l'écran avec des ombres RenderTexture comme matériau;
  7. La caméra principale dessine un plan à l'écran avec une texture de rendu d'entités en tant que matériau.

Un tel gâteau en couches.

Dans la capture d'écran ci-dessous, la caméra de l'éditeur est configurée en mode tridimensionnel pour montrer l'emplacement des couches les unes par rapport aux autres.



Nuances


Mais comme il s'est avéré au cours du processus de réplication de la décision à d'autres entités, le cas général n'a pas couvert tous les scénarios possibles. Par exemple, il y avait des entités qui étaient à une certaine hauteur par rapport à la surface, en particulier les coquilles et certains personnages de cinématiques. De plus, les obus avaient également la possibilité de tourner en fonction de la direction de leur mouvement sur l'écran, en raison de quoi, en plus de définir le point d'intersection de l'objet et de son ombre, il était nécessaire de sélectionner la partie tournante en tant qu'objet enfant séparé, pour corriger la logique de rotation du projectile et leur animation.

La capture d'écran suivante montre un exemple de rotation des coques et de leurs ombres.



Les personnages volants, comme les foules volantes planifiées, peuvent également se déplacer dans leurs coordonnées Y virtuelles, ce qui a nécessité la création d'un mécanisme pour calculer la position de l'ombre à partir de la position de son propriétaire sur l'axe Y virtuel.

Le GIF ci-dessous montre un exemple de déplacement d'un objet en hauteur.



Un autre cas qui est sorti du concept général était un char. Contrairement à toutes les autres entités, le réservoir a une taille très importante le long de l'axe Z virtuel, et la mise en œuvre globale des ombres, comme déjà mentionné, nécessite que l'objet soit presque plat. Le moyen le plus simple de contourner ce problème était de dessiner manuellement des formes d'ombre pour des parties individuelles du réservoir, car vous pouviez placer n'importe quoi sur la couche d'ombre.

Pour la construction correcte des ombres dessinées à la main, j'ai dû assembler une conception de lignes basée sur une capture d'écran d'une ombre existante, qui peut être vue dans la capture d'écran ci-dessous.



Si vous mettez à l'échelle et placez cette structure de manière à ce que la partie supérieure soit à un certain point de l'objet parent et que la partie inférieure soit au point de contact avec la surface, le coin droit de la structure indiquera l'endroit où le point d'ombre correspondant devrait être. Après avoir projeté plusieurs points clés de cette manière, il n'est pas difficile de construire toute l'ombre sur eux.

De plus, les parties individuelles du réservoir pouvaient avoir des hauteurs différentes pour fixer des parties enfants, ce qui, comme dans le cas des personnages volants et des foules, nécessitait un ajustement de la position de l'ombre de chaque partie spécifique.

La capture d'écran ci-dessous montre le réservoir, son assemblage d'ombre et il se présente également sous la forme de pièces séparées.



Les ombres des murs se sont avérées être une douleur distincte. Au moment du début des travaux sur les ombres, les murs étaient de la même nature que les détails du réservoir - un objet de plusieurs dizaines de sprites séparés. Cependant, les murs avaient plusieurs états contrôlés par l'animateur.

En réfléchissant bien à ce que j'en ferais, j'en suis venu à la conclusion que le concept des murs doit être changé. En conséquence, les murs ont été divisés en sections, chacune ayant son propre ensemble d'états, son propre animateur et sa propre ombre. Cela a permis d'utiliser la même approche pour créer des ombres pour les mobs parallèles à l'axe X, comme pour les mobs, et pour les sections qui ne correspondaient pas à cette règle, ils devaient trouver quelque chose qui leur était propre. Dans certains cas, j'ai dû créer mon propre animateur pour l'ombre de la section et définir manuellement la position des sprites.

Par exemple, dans le cas de la section illustrée dans la capture d'écran ci-dessous, l'ombre est créée en appliquant une distorsion pour chaque journal individuel au lieu de la section entière.



Conclusion


En fait, c'est tout. Malgré toutes les nuances ci-dessus, la tâche d'origine a été achevée dans son intégralité, et maintenant mon projet possède des ombres d'aspect assez décent, bien que d'origine quelque peu douteuse. J'espère, grâce à cet article, pour le prochain développeur indépendant qui m'a posé une question similaire, Internet deviendra un peu plus utile, sinon comme un exemple à suivre, puis au moins comme une erreur de quelqu'un d'autre pour votre propre apprentissage.

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


All Articles