Ce que vous obtiendrez de cette page: des conseils de Michelle Martin, ingénieur logiciel chez MetalPop Games, sur la manière d'optimiser les jeux pour une gamme d'appareils mobiles, afin d'atteindre le plus grand nombre possible de joueurs potentiels.
Avec son jeu de stratégie mobile Galactic Colonies, MetalPop Games a dû relever le défi de permettre aux joueurs de construire d'immenses villes sur leurs appareils bas de gamme, sans que leur taux de rafraîchissement ne chute ou que leur appareil ne surchauffe. Découvrez comment ils ont trouvé l'équilibre entre des visuels attrayants et des performances solides.
Aussi puissants que soient les appareils mobiles aujourd'hui, il est toujours difficile de faire tourner des environnements de jeu vastes et attrayants à un taux de rafraîchissement élevé. Obtenir un taux de rafraîchissement de 60 images par seconde dans un environnement 3D à grande échelle sur un appareil mobile ancien peut être un véritable défi.
En tant que développeurs, nous pourrions nous contenter de cibler les téléphones haut de gamme et partir du principe que la plupart des joueurs disposent d'un matériel suffisant pour faire tourner notre jeu sans problème. Mais cela aura pour effet d'exclure un grand nombre d'acteurs potentiels, car de nombreux appareils plus anciens sont encore utilisés. Ce sont autant de clients potentiels que vous ne voulez pas exclure.
Dans notre jeu, Galactic Colonies, les joueurs colonisent des planètes étrangères et construisent d'immenses colonies composées d'un grand nombre de bâtiments individuels. Alors que les petites colonies ne comptent qu'une douzaine de bâtiments, les plus grandes peuvent facilement en avoir des centaines.
Voici quelle était notre liste d'objectifs quand nous avons commencé à concevoir notre pipeline :
- nous voulons des cartes énormes avec de nombreux bâtiments,
- Nous voulons fonctionner rapidement sur des appareils mobiles moins chers et/ou plus anciens.
- nous voulons un éclairage et des ombres jolis,
- nous voulons un pipeline de production simple et facile à entretenir.
Un bon éclairage dans votre jeu est essentiel pour que les modèles 3D soient superbes. Dans Unity, c'est très simple : configurez votre niveau, placez vos lumières dynamiques et c'est parti. Et si vous avez besoin de garder un œil sur les performances, il vous suffit de cuire toutes vos lumières et d'ajouter des SSAO et d'autres effets visuels par le biais de la pile de post-traitement. Voilà, c'est parti !
Pour les jeux mobiles, vous avez besoin d'un bon bagage d'astuces et de solutions de contournement pour mettre en place l'éclairage. Par exemple, à moins que vous ne visiez des appareils haut de gamme, vous ne devriez pas utiliser d'effets de post-traitement. De même, une grande scène pleine de lumières dynamiques réduira considérablement votre framerate.
L'éclairage en temps réel peut être coûteux sur un PC de bureau. Sur les appareils mobiles, les ressources sont encore plus limitées et vous ne pouvez pas toujours vous offrir toutes les fonctions intéressantes que vous aimeriez avoir.
Vous ne voulez donc pas épuiser les batteries des téléphones de vos utilisateurs plus que nécessaire en ajoutant trop de lumières fantaisistes à votre scène.
Si vous repoussez constamment les limites du matériel, le téléphone va chauffer et, par conséquent, ralentir pour se protéger. Pour éviter cela, il est possible d'étouffer toutes les lumières qui ne projettent pas d'ombres en temps réel.
Le processus de cuisson de la lumière consiste à calculer au préalable les ombres et les lumières d'une scène (statique), dont les informations sont ensuite stockées dans une carte lumineuse. Le moteur de rendu sait alors où éclaircir ou assombrir un modèle, créant ainsi l'illusion de la lumière.
Le rendu des choses de cette manière est rapide parce que tous les calculs coûteux et lents de la lumière ont été effectués hors ligne et qu'au moment de l'exécution, le moteur de rendu (shader) n'a plus qu'à rechercher le résultat dans une texture.
En contrepartie, vous devrez fournir des textures supplémentaires pour la carte lumineuse, ce qui augmentera la taille de votre compilation et nécessitera une mémoire de texture supplémentaire au moment de l'exécution. Vous perdrez également un peu d'espace car vos mailles auront besoin d'UVs lightmap et deviendront un peu plus grandes. Mais dans l'ensemble, vous bénéficierez d'un gain de vitesse considérable.
Mais pour notre jeu, ce n'était pas une option, puisque le monde du jeu est construit en temps réel par le joueur. Le fait que de nouvelles régions soient constamment découvertes, que de nouveaux bâtiments soient ajoutés ou que des bâtiments existants soient modernisés, empêche toute forme de cuisson efficace de la lumière. Il ne suffit pas d'appuyer sur le bouton " Bake" pour avoir un monde dynamique qui peut être modifié en permanence par le joueur.
Nous avons donc été confrontés à un certain nombre de problèmes qui se posent lorsqu'il s'agit de créer des éclairages pour des scènes très modulaires.
Dans Unity, les données de cuisson de la lumière sont stockées et directement associées aux données de la scène. Ce n'est pas un problème si vous avez des niveaux individuels et des scènes préconstruites et seulement une poignée d'objets dynamiques. Vous pouvez précuire l'éclairage et avoir terminé.
Évidemment, cela ne fonctionne pas lorsque vous créez des niveaux de manière dynamique. Dans un jeu de construction de ville, le monde n'est pas créé à l'avance. Au lieu de cela, il est en grande partie assemblé de manière dynamique et à la volée par le joueur qui décide quoi construire et où le construire. Cela se fait généralement en instanciant des préfabriqués à l'endroit où le joueur décide de construire quelque chose.
La seule solution à ce problème est de stocker toutes les données relatives à la cuisson de la lumière dans le préfabriqué plutôt que dans la scène. Malheureusement, il n'existe pas de moyen simple de copier les données de la carte lumineuse à utiliser, ses coordonnées et son échelle dans un préfabriqué.
La meilleure approche pour obtenir un pipeline solide qui gère les Prefabs cuits à la lumière est de créer les Prefabs dans une scène différente, séparée (plusieurs scènes, en fait) et de les charger ensuite dans le jeu principal lorsque c'est nécessaire. Chaque pièce modulaire est cuite à la lumière et sera ensuite chargée dans le jeu en cas de besoin.
Regardez de près comment fonctionne le light baking dans Unity et vous verrez que le rendu d'un mesh light baked consiste simplement à lui appliquer une autre texture et à éclaircir, assombrir (ou parfois coloriser) un peu le mesh. Tout ce dont vous avez besoin, c'est de la texture de la carte lumineuse et des coordonnées UV, qui sont toutes deux créées par Unity pendant le processus de cuisson de la lumière.
Pendant la cuisson de la lumière, le processus Unity crée un nouvel ensemble de coordonnées UV (qui pointent vers la texture de la carte lumineuse), ainsi qu'un décalage et une échelle pour le maillage individuel. Le fait de recuire les lumières modifie ces coordonnées à chaque fois.
Pour trouver une solution à ce problème, il est utile de comprendre comment fonctionnent les canaux UV et comment les utiliser au mieux.
Chaque maillage peut avoir plusieurs ensembles de coordonnées UV (appelés canaux UV dans Unity). Dans la majorité des cas, un seul jeu d'UV suffit, car les différentes textures (Diffuse, Spec, Bump, etc.) stockent toutes les informations au même endroit dans l'image.
Mais lorsque des objets partagent une texture, telle qu'une carte lumineuse, et qu'ils doivent rechercher les informations d'un endroit spécifique dans une grande texture, il n'y a souvent aucun moyen d'ajouter un autre jeu d'UV à utiliser avec cette texture partagée.
L'inconvénient des coordonnées UV multiples est qu'elles consomment de la mémoire supplémentaire. Si vous utilisez deux jeux d'UV au lieu d'un, vous doublez le nombre de coordonnées UV pour chacun des sommets du maillage. Chaque sommet stocke désormais deux nombres à virgule flottante, qui sont téléchargés vers le GPU lors du rendu.
Unity génère les coordonnées et la carte lumineuse, en utilisant la fonctionnalité habituelle de cuisson de la lumière. Le moteur écrira les coordonnées UV de la carte lumineuse dans le deuxième canal UV du modèle. Il est important de noter que l'ensemble primaire de coordonnées UV ne peut pas être utilisé pour cela, car le modèle doit être déballé.
Imaginez une boîte utilisant la même texture pour chacune de ses faces : Les différents côtés de la boîte ont tous les mêmes coordonnées UV, car ils réutilisent la même texture. Mais cela ne fonctionnera pas pour un objet mappé en lumière, puisque chaque côté de la boîte est touché par les lumières et les ombres individuellement. Chaque côté a besoin de son propre espace dans la carte lumineuse avec ses propres données d'éclairage. D'où la nécessité d'une nouvelle série d'UV.
Pour créer un nouveau préfabriqué cuit à la lumière, il suffit de stocker la texture et ses coordonnées pour qu'elles ne soient pas perdues et de les copier dans le préfabriqué.
Une fois le light baking effectué, nous lançons un script qui parcourt tous les maillages de la scène et écrit les coordonnées UV dans le canal UV2 réel du maillage, avec les valeurs de décalage et de mise à l'échelle appliquées.
Le code permettant de modifier les maillages est relativement simple (voir l'exemple ci-dessous).
Pour être plus précis : Cette opération est effectuée sur une copie des maillages, et non sur l'original, car nous procéderons à d'autres optimisations de nos maillages au cours du processus de cuisson.
Les copies sont automatiquement générées, sauvegardées dans un Prefab et dotées d'un nouveau matériau avec un shader personnalisé et le lightmap nouvellement créé. Nos maillages d'origine restent ainsi intacts et les Prefabs cuits à la lumière sont immédiatement prêts à l'emploi.
Le flux de travail est ainsi très simple. Pour mettre à jour le style et l'aspect des graphiques, il suffit d'ouvrir la scène appropriée, d'effectuer toutes les modifications jusqu'à ce que vous soyez satisfait, puis de lancer le processus automatisé de cuisson et de copie. Une fois ce processus terminé, le jeu commencera à utiliser les Prefabs et les meshes mis à jour avec l'éclairage mis à jour.
La texture de la carte lumineuse est ajoutée par un shader personnalisé, qui applique la carte lumineuse comme une deuxième texture lumineuse au modèle pendant le rendu. Le shader est très simple et court, et en plus d'appliquer la couleur et la carte de lumière, il calcule un faux effet spéculaire/brillant bon marché.
Voici le code du shader ; l'image ci-dessus est celle d'une configuration de matériau utilisant ce shader.
Dans notre cas, nous avons quatre scènes différentes avec tous les Prefabs installés. Notre jeu comporte différents biomes tels que les tropiques, la glace, le désert, etc. et nous répartissons nos scènes en conséquence.
Tous les préfabriqués utilisés dans une scène donnée partagent une seule carte lumineuse. Cela signifie une seule texture supplémentaire, en plus du fait que les préfabriqués ne partagent qu'un seul matériau. En conséquence, nous avons pu rendre tous les modèles statiques et rendre par lots la quasi-totalité de notre monde en un seul appel de dessin.
Les scènes de cuisson de la lumière, dans lesquelles toutes nos tuiles/bâtiments sont installés, ont des sources de lumière supplémentaires pour créer des reflets localisés. Vous pouvez placer autant de lumières que nécessaire dans les scènes d'installation puisqu'elles seront toutes cuites de toute façon.
Le processus de cuisson est géré dans une boîte de dialogue personnalisée qui prend en charge toutes les étapes nécessaires. Il veille à ce que :
- chaque maillage s'est vu assigner le matériau approprié,
- Tout ce qui n'a pas besoin d'être cuit pendant le processus est caché
- Les mailles sont combinées/cuites
- Les UV sont copiés et les Prefabs créés.
- Tout est nommé correctement et les fichiers nécessaires du système de contrôle de version sont extraits.
Des Prefabs correctement nommés sont créés à partir des maillages afin que le code du jeu puisse les charger et les utiliser directement. Les métafichiers sont également modifiés au cours de ce processus, afin que les références aux maillages des préfabriqués ne soient pas perdues.
Ce flux de travail nous permet de peaufiner nos bâtiments autant que nous le souhaitons, de les éclairer comme nous l'entendons et de laisser le script s'occuper de tout.
Lorsque nous revenons à notre scène principale et que nous lançons le jeu, il fonctionne tout simplement - aucune intervention manuelle ou autre mise à jour n'est nécessaire.
L'un des inconvénients évidents d'une scène dans laquelle 100 % de l'éclairage est précuit est qu'il est difficile d'avoir des objets dynamiques ou des mouvements. Tout ce qui projette une ombre nécessiterait un calcul en temps réel de la lumière et de l'ombre, ce que nous aimerions bien sûr éviter complètement.
Mais sans objets en mouvement, l'environnement 3D apparaîtrait statique et mort.
Nous étions bien sûr prêts à accepter certaines restrictions, car notre priorité absolue était d'obtenir de bons visuels et un rendu rapide. Pour créer l'impression d'une colonie spatiale ou d'une ville vivante et en mouvement, il n'est pas nécessaire d'avoir beaucoup d'objets qui se déplacent réellement. Et la plupart d'entre elles ne nécessitaient pas nécessairement des ombres, ou du moins leur absence ne serait pas remarquée.
Nous avons commencé par diviser tous les blocs de construction de la ville en deux préfabriqués distincts. Une partie statique, qui contient la majorité des sommets, tous les éléments complexes de nos maillages, et une partie dynamique, qui contient le moins de sommets possible.
Les parties dynamiques d'un préfabriqué sont des éléments animés placés au-dessus des parties statiques. Ils ne sont pas cuits à la lumière et nous avons utilisé un shader de faux éclairage très rapide et bon marché pour créer l'illusion que l'objet est dynamiquement éclairé.
Les objets n'ont pas d'ombre ou nous avons créé une fausse ombre dans le cadre de la partie dynamique. La plupart de nos surfaces sont planes, ce qui, dans notre cas, n'a pas constitué un obstacle majeur.
Il n'y a pas d'ombres sur les parties dynamiques, mais c'est à peine perceptible, à moins que vous ne sachiez le chercher. L'éclairage des préfabriqués dynamiques est également faux - il n'y a pas du tout d'éclairage en temps réel.
Le premier raccourci bon marché que nous avons pris a été de coder en dur la position de notre source de lumière (le soleil) dans le shader de faux éclairage. C'est une variable de moins que le shader doit chercher et remplir dynamiquement à partir du monde.
Il est toujours plus rapide de travailler avec une constante qu'avec une valeur dynamique. Cela nous a permis d'obtenir un éclairage de base, des côtés clairs et sombres des mailles.
Pour rendre les choses un peu plus brillantes, nous avons ajouté un faux calcul spéculaire/brillant aux shaders pour les objets dynamiques et statiques. Les reflets spéculaires permettent de créer un aspect métallique tout en traduisant la courbure d'une surface.
Les reflets spéculaires étant une forme de réflexion, l'angle de la caméra et de la source lumineuse l'une par rapport à l'autre est nécessaire pour les calculer correctement. Lorsque la caméra se déplace ou pivote, le spéculaire change. Tout calcul de shader nécessiterait l'accès à la position de la caméra et à chaque source de lumière dans la scène.
Cependant, dans notre jeu, nous n'avons qu'une seule source de lumière que nous utilisons pour le spéculaire : le soleil. Dans notre cas, le soleil ne bouge jamais et peut être considéré comme une lumière directionnelle. Nous pouvons simplifier considérablement le shader en n'utilisant qu'une seule lumière et en supposant une position et un angle d'arrivée fixes.
Mieux encore, notre caméra dans Galactic Colonies montre la scène de haut en bas, comme dans la plupart des jeux de construction de villes. L'appareil photo peut être légèrement incliné et faire un zoom avant et arrière, mais il ne peut pas tourner autour de l'axe vertical.
Dans l'ensemble, il s'agit toujours de regarder l'environnement d'en haut. Pour simuler un effet spéculaire bon marché, nous avons fait comme si la caméra était complètement fixe et que l'angle entre la caméra et la lumière était toujours le même.
De cette manière, nous pourrions à nouveau coder en dur une valeur constante dans le shader et obtenir ainsi un effet spec/gloss bon marché.
L'utilisation d'un angle fixe pour le spéculaire est bien sûr techniquement incorrecte, mais il est pratiquement impossible de faire la différence tant que l'angle de la caméra ne change pas beaucoup.
Pour le joueur, la scène restera correcte, ce qui est tout l'intérêt de l'éclairage en temps réel.
L'éclairage d'un environnement dans un jeu vidéo en temps réel est et a toujours été une question d'apparence visuelle correcte, plutôt que de simulation physique correcte.
Étant donné que presque tous nos maillages partagent un seul matériau et que la plupart des détails proviennent de la carte lumineuse et des sommets, nous avons ajouté une carte de texture spéculaire pour indiquer au shader où et quand appliquer la valeur spéculaire, et avec quelle intensité. L'accès à la texture se fait par le canal UV primaire et ne nécessite donc pas de coordonnées supplémentaires. Et comme il n'y a pas beaucoup de détails, il est de très faible résolution et n'occupe que très peu d'espace.
Pour certains de nos petits éléments dynamiques à faible nombre de vertex, nous pourrions même utiliser la mise en lots dynamique automatique d'Unity, ce qui accélèrerait encore le rendu.
Toutes ces ombres cuites peuvent parfois créer de nouveaux problèmes, surtout lorsque l'on travaille avec des bâtiments relativement modulaires. Dans un cas, nous avions un entrepôt que le joueur pouvait construire et qui affichait le type de marchandises stockées sur le bâtiment lui-même.
Cela pose des problèmes car nous avons un objet cuit à la lumière sur un objet cuit à la lumière. Lightbake-ception!
Nous avons abordé le problème en utilisant une autre astuce bon marché :
- la surface où l'objet supplémentaire devait être ajouté devait être plate et d'un gris comparable au bâtiment de base,
- ce compromis nous permettait de précalculer les objets sur une petite surface plate et de les placer sur la zone avec juste un léger décalage,
- l'éclairage, les points lumineux, les lueurs de couleur et les ombres étaient tous précalculés dans la tuile.
Construire et cuire nos Prefabs de cette façon nous permet d'avoir des cartes énormes avec des centaines de bâtiments tout en gardant un nombre d'appels de tirage très bas. Tout notre monde de jeu est plus ou moins rendu avec un seul matériau et nous en sommes à un point où l'interface utilisateur utilise plus d'appels de dessin que notre monde de jeu. Moins Unity a de matériaux différents à rendre, mieux c'est pour les performances de votre jeu.
Cela nous laisse une grande marge de manœuvre pour ajouter d'autres éléments à notre monde, tels que des particules, des effets météorologiques et d'autres éléments de plaisir visuel.
Ainsi, même les joueurs équipés d'appareils plus anciens peuvent construire de grandes villes avec des centaines de bâtiments tout en conservant une vitesse stable de 60 images par seconde.