Optimisation des Variantes de Shaders Unity & Conseils de Dépannage

ATTILIO CAROTENUTO / UNITYTechnical Lead
May 28, 2024|15 Min
Optimisation des Variantes de Shaders Unity & Conseils de Dépannage
Cette page a été traduite automatiquement pour faciliter votre expérience. Nous ne pouvons pas garantir l'exactitude ou la fiabilité du contenu traduit. Si vous avez des doutes quant à la qualité de cette traduction, reportez-vous à la version anglaise de la page web.

Lorsque vous écrivez des shaders dans Unity, nous avons la possibilité d'inclure plusieurs fonctionnalités, passes et logiques de branchement dans un seul fichier source. Au moment de la construction, les fichiers source des shaders sont compilés en programmes de shaders, qui contiennent une ou plusieurs variantes. Une variante est une version de ce shader suivant un ensemble unique de conditions, résultant (dans la plupart des cas) en un chemin d'exécution linéaire sans conditionnelles de branchement statiques.

La raison pour laquelle nous utilisons des variantes, plutôt que de garder tous les chemins de branchement dans un seul shader, est que les GPU sont excellents pour paralléliser le code qui est prévisible et suit toujours le même chemin, ce qui entraîne un débit plus élevé. Si des conditionnelles sont présentes dans le programme de shader compilé, le GPU devra dépenser des ressources à effectuer des tâches prédictives, attendant que les autres chemins soient complétés, et ainsi de suite, introduisant des inefficacités.

Bien que cela conduise à des performances GPU significativement meilleures par rapport au branchement dynamique, cela a aussi quelques inconvénients. Les temps de construction vont s'allonger à mesure que le nombre de variantes augmente, parfois même de plusieurs heures par construction. Le jeu mettra également plus de temps à démarrer, car il devra passer plus de temps à charger et à préchauffer les shaders. Enfin, vous pourriez remarquer une utilisation significative de la mémoire à l'exécution provenant des shaders si les variantes ne sont pas correctement gérées, parfois plus de 1 Go.

Le nombre de variantes générées augmente en fonction d'une variété de facteurs, y compris les mots-clés et les propriétés définis, les paramètres de qualité, les niveaux graphiques, les API graphiques activées, les effets de post-traitement, le pipeline de rendu actif, les modes d'éclairage et de brouillard, et si XR est activé, entre autres. Les shaders qui entraînent un grand nombre de variantes sont souvent appelés uber shaders. À l'exécution, Unity charge la variante qui correspond aux paramètres et mots-clés requis, comme nous le verrons plus tard.

C'est particulièrement impactant lorsque vous considérez que nous voyons souvent des shaders avec plus de 100 mots-clés, entraînant un nombre ingérable de variantes résultantes, souvent appelées explosion de variantes de shader. Il n'est pas inhabituel de voir des shaders avec un espace de variante initial dans les millions avant qu'aucun filtrage ne soit appliqué.

Pour atténuer cela, Unity essaiera de réduire le nombre de variantes générées en fonction de quelques passes de filtrage. Par exemple, si XR n'est pas activé, les variantes nécessaires pour cela seront normalement supprimées. Unity prend ensuite en compte les fonctionnalités que vous utilisez réellement dans vos scènes, telles que les modes d'éclairage, le brouillard, et ainsi de suite. Celles-ci sont particulièrement difficiles à repérer, car les développeurs et les artistes pourraient introduire des changements apparemment sûrs qui entraînent en réalité une augmentation significative des variantes de shader, sans moyen évident de détecter à moins que vous ne mettiez en place des mesures de protection dans le cadre de votre pipeline de déploiement.

Bien que cela soit utile, ce processus n'est pas parfait, et il y a beaucoup de choses que nous pouvons faire pour supprimer autant de variantes que possible sans affecter la qualité visuelle de votre jeu.

Ici, j'aimerais partager quelques conseils pratiques sur la façon de gérer les variantes, comprendre d'où elles viennent, et quelques moyens efficaces de les réduire. Le temps de construction de votre projet et son empreinte mémoire en bénéficieront grandement.

Pour plus d'informations sur la suppression des variantes de shader, consultez Réduction des variantes de shader dans le Manuel Unity.

Comprendre l'impact des mots-clés sur les variantes

Les variantes de shader sont générées en fonction de toutes les combinaisons possibles de shader_feature et de multi_compile utilisés dans votre shader, parmi d'autres facteurs. Les mots-clés marqués comme multi_compile sont toujours inclus dans votre construction, tandis que ceux marqués comme shader_feature seront inclus s'ils sont référencés par un matériau dans votre projet. Pour cette raison, vous devriez utiliser shader_feature chaque fois que possible.

Pour voir quels mots-clés sont définis dans un shader, vous pouvez le sélectionner et vérifier l'Inspecteur.

Mots-clés de la vue Inspecteur de Shader

Comme vous pouvez le voir, les mots-clés sont divisés en Remplaçables et Non Remplaçables. Les mots-clés locaux (ceux définis dans le fichier shader réel) avec une portée globale peuvent être remplacés par un mot-clé shader global avec un nom correspondant. S'ils sont définis à une portée locale (en utilisant multi_compile_local ou shader_feature_local), ils ne peuvent pas être remplacés et apparaîtront dans la section Non remplaçable en dessous. Les mots-clés de shader globaux sont fournis par le moteur Unity, et ils sont remplaçables. Comme ils peuvent être ajoutés à tout moment dans le processus de construction, tous les mots-clés globaux ne peuvent pas apparaître dans cette liste.

Les mots-clés peuvent être définis dans des groupes mutuellement exclusifs, appelés ensembles, en les définissant dans la même directive. En faisant cela, vous évitez de générer des variantes pour des combinaisons de mots-clés qui ne seront jamais activées en même temps (comme deux types différents d'éclairage ou de brouillard).

#pragma shader_feature LIGHT_LOW_Q LIGHT_HIGH_Q

Pour réduire le nombre de mots-clés par plateforme, vous pouvez utiliser des macros de préprocesseur pour les définir uniquement pour la plateforme pertinente, par exemple :

#ifdef SHADER_API_METAL
   #pragma shader_feature IOS_FOG_FEATURE
#else
   #pragma shader_feature BASE_FOG_FEATURE
#endif

Notez que ces expressions avec des macros ne peuvent pas dépendre d'autres mots-clés ou fonctionnalités qui ne sont pas uniquement liées à la cible de construction.

Les mots-clés peuvent également être limités à un passage spécifique, réduisant le nombre de combinaisons potentielles. Pour ce faire, vous pouvez ajouter l'un des suffixes suivants à la directive :

  • _vertex
  • _fragment
  • _hull
  • _domain
  • _geometry
  • _raytracing

Par exemple :

#pragma shader_feature_fragment FRAG_FEATURE_1 FRAG_FEATURE_2

Cela peut se comporter différemment selon le moteur de rendu que vous utilisez. Par exemple, sur OpenGL, OpenGL ES et Vulkan, les suffixes seront ignorés.

Vous pouvez utiliser la directive #pragma skip_variants pour définir des mots-clés qui doivent être exclus lors de la génération de variantes pour ce shader spécifique. Lors de la création de votre build de joueur, toutes les variantes de shader pour ce shader contenant l'un de ces mots-clés seront ignorées.

Vous pouvez également définir des mots-clés en option en utilisant la directive #pragma dynamic_branch, ce qui obligera Unity à s'appuyer sur le branchement dynamique et à ne pas générer de variantes pour ces mots-clés. Bien que cela réduise le nombre de variantes résultantes, cela peut entraîner des performances GPU plus faibles selon le shader et le contenu du jeu, il est donc recommandé de profiler en conséquence lors de son utilisation.

Pour plus d'informations sur les mots-clés de shader, reportez-vous à Changer le fonctionnement des shaders en utilisant des mots-clés dans le Manuel Unity.

Inspection du code shader généré

Normalement, les variantes de shader ne seront pas compilées tant que vous ne construisez pas réellement le jeu. En utilisant cette option, vous pouvez inspecter les variantes de shader résultantes pour une plateforme de build spécifique ou une API graphique. Cela vous permet de vérifier les erreurs à l'avance. De plus, vous pouvez coller le code généré dans des outils d'analyse de performance de shader GPU, tels que PVRShaderEditor, pour d'autres optimisations.

Mots-clés de la vue Inspecteur de Shader

En bas, vous remarquerez une entrée indiquant combien de variantes sont incluses, en fonction des matériaux présents dans la scène actuellement ouverte, sans aucune suppression scriptable appliquée. Si vous appuyez sur le bouton Afficher, il affichera un fichier temporaire avec des informations de débogage supplémentaires sur les mots-clés qui ont été utilisés ou supprimés sur diverses plateformes, y compris le nombre de variantes de stade de vertex.

La case à cocher Prétraiter uniquement ci-dessus vous permet de basculer entre le code shader compilé et le code source de shader prétraité pour un débogage plus facile et plus rapide.

Si vous utilisez le pipeline de rendu intégré et travaillez avec un shader de surface, vous avez la possibilité de vérifier le code généré que Unity utilisera pour remplacer votre code source de shader simplifié lors de la construction. Vous pouvez ensuite remplacer en option votre code source de shader par le code généré, si vous souhaitez modifier la sortie.

Pour plus d'informations, reportez-vous à Vérifiez combien de variantes de shader vous avez dans le Manuel Unity.

Option de code généré affiché pour un shader de surface Texte alternatif : Activation de l'option Afficher le code généré pour un shader de surface
Déterminer quels variants sont générés au moment de la construction

Lors de la construction du jeu, Unity déterminera l'espace des variants pour chaque shader en fonction de toutes les permutations possibles de ses fonctionnalités, des paramètres du moteur et d'autres facteurs. Ces combinaisons sont ensuite transmises aux préprocesseurs pour plusieurs passes de stripping. Cela peut être étendu en utilisant des rappels IPreprocessShaders pour créer une logique personnalisée afin de retirer plus de variants de la construction, comme expliqué ci-dessous.

Les shaders qui sont inclus dans la liste des shaders toujours inclus (sous Paramètres du projet > Graphiques) auront tous leurs variants inclus dans la construction. Pour cette raison, il est préférable de l'utiliser uniquement lorsque cela est strictement nécessaire, car cela peut facilement entraîner un grand nombre de variants générés.

Enfin, le pipeline de construction passera par un processus appelé dé-duplication, identifiant les variants identiques au sein du même Pass et s'assurant qu'ils pointent vers le même bytecode. Cela entraînera une réduction de la taille sur le disque, mais les variants identiques affecteront toujours négativement le temps de construction, le temps de chargement et l'utilisation de la mémoire à l'exécution, donc ce n'est pas un remplacement pour un stripping approprié des variants.

Après une construction réussie, nous pouvons consulter le fichier Editor.log pour collecter des informations utiles sur les variants de shaders qui ont été inclus dans la construction. Pour ce faire, recherchez dans le fichier journal "Compilation du shader" et le nom de votre shader. Voici par exemple à quoi cela ressemble :

Compiling shader "GameShaders/MyShader" pass "Pass 1" (vp)
	Full variant space:     	608
	After settings filtering:   608
	After built-in stripping:   528
	After scriptable stripping: 528
	Processed in 0.00 seconds
	starting compilation...
	finished in 0.02 seconds. Local cache hits 528 (0.16s CPU time), remote cache hits 0 (0.00s CPU time), compiled 0 variants (0.00s CPU time), skipped 0 variants

Dans certains cas, vous pourriez voir le nombre de variants augmenter après l'étape de filtrage des paramètres, par exemple si votre projet a XR activé.

Si votre jeu prend en charge plusieurs API graphiques, vous trouverez également des informations pour chaque moteur de rendu pris en charge :

Serialized binary data for shader GameShaders/MyShader in 0.00s
	gles3 (total internal programs: 290, unique: 193)
	vulkan (total internal programs: 290, unique: 193)

Enfin, vous verrez ces journaux de compression qui vous donneront une indication de la taille finale, sur le disque, du shader pour une API graphique spécifique :

Compressed shader 'GameShaders/MyShader' on vulkan from 1.35MB to 0.19MB

Si vous utilisez le Universal Render Pipeline (URP), vous pouvez choisir de générer des journaux uniquement à partir des shaders SRP, de tous les shaders ou de désactiver les journaux. Pour ce faire, sélectionnez le Niveau de journalisation dans Paramètres du projet > Graphiques > Paramètres globaux URP.

Définir le niveau de journalisation dans les paramètres globaux URP

De plus, si vous sélectionnez l'option Exporter les variants de shaders ci-dessous, un fichier JSON sera généré après votre construction contenant un rapport des compilations de variants de shaders. Cela est disponible sur Unity 2022.2 ou version ultérieure.

Déterminer quels variants sont utilisés à l'exécution

Pour comprendre quels shaders sont réellement compilés pour le GPU à l'exécution, vous pouvez activer l'option Journaliser la compilation des shaders, sous Paramètres du projet > Graphiques.

Activer la journalisation de la compilation des shaders dans les paramètres de projet graphiques

Cela fera en sorte que votre jeu imprime dans les journaux du joueur chaque fois qu'un shader est compilé pendant que vous jouez. Cela ne fonctionnera que sur les versions de développement et en mode Débogage, comme décrit dans l'info-bulle.

Le format ressemble à ceci :

Compiled Shader: Folder/ShaderName, pass: PASS_NAME, stage: STAGE_NAME, keywords ACTIVE_KEYWORD_1 ACTIVE_KEYWORD_2

Gardez à l'esprit que certaines plateformes, comme Android, mettront en cache les shaders compilés. Pour cette raison, vous devrez peut-être désinstaller et réinstaller le jeu avant de faire un test pour capturer tous les shaders compilés.

Enfin, vous pouvez utiliser le package Memory Profiler pour prendre un instantané de votre jeu pendant qu'il fonctionne, puis avoir un aperçu des shaders actuellement chargés en mémoire et de leur taille. Le tri par taille donne normalement une bonne indication des shaders qui apportent le plus de variantes et qui valent la peine d'être optimisés.

Aperçu des shaders dans le Memory Profiler
Élagage basé sur les paramètres graphiques

Dans le cadre des passes d'élagage, Unity supprimera les variantes de shader liées aux fonctionnalités graphiques que votre jeu n'utilise pas. Le processus change légèrement si vous utilisez le pipeline de rendu intégré ou URP.

Pour les définir, allez dans Paramètres du projet > Graphiques. À partir de là, tout en utilisant le pipeline de rendu intégré, vous pouvez sélectionner les modes de Lightmap et de brouillard que votre jeu prend en charge.

Paramètres d'élagage des shaders graphiques

Les définir sur Automatique permet à Unity de déterminer quelles variantes élaguer en fonction des scènes incluses dans votre build.

Si vous n'êtes pas sûr des fonctionnalités que vous utilisez, vous pouvez également utiliser le bouton Importer depuis la scène actuelle pour laisser Unity déterminer les fonctionnalités dont vous avez besoin. Bien sûr, cela n'est utile que si toutes vos scènes utilisent les mêmes paramètres, alors assurez-vous de sélectionner une scène représentative lorsque vous utilisez cette option.

Si vous utilisez URP, certaines de ces options seront cachées. Au lieu de cela, vous pourrez définir les fonctionnalités requises par votre jeu directement dans l'actif des paramètres de pipeline.

Par exemple, désactiver les trous de terrain entraînera la suppression de toutes les variantes de shader de trous de terrain, réduisant ainsi le temps de construction.

URP offre un contrôle plus granulaire sur les fonctionnalités que vous souhaitez inclure dans votre jeu, ce qui peut entraîner des constructions plus optimisées avec moins de variantes inutilisées.

Suppression basée sur les niveaux graphiques

Remarque : Ceci n'est pertinent que lors de l'utilisation du pipeline de rendu intégré. Ces paramètres seront ignorés lors de l'utilisation d'un pipeline de rendu scriptable tel que URP.

Les niveaux graphiques sont utilisés pour appliquer différents paramètres graphiques en fonction du matériel sur lequel votre jeu s'exécute (à ne pas confondre avec les paramètres de qualité). Lorsque le jeu démarre, Unity déterminera le niveau graphique de votre appareil en fonction des capacités matérielles, de l'API graphique et d'autres facteurs.

Ils peuvent être définis dans Paramètres du projet > Graphiques > Paramètres de niveau.

Paramètres de niveau graphique

Sur cette base, Unity ajoute ces trois mots-clés à tous les shaders :

UNITY_HARDWARE_TIER1

UNITY_HARDWARE_TIER2

UNITY_HARDWARE_TIER3

Il génère ensuite des variantes de shader pour chacun des niveaux graphiques définis. Si vous n'utilisez pas de niveaux graphiques et souhaitez éviter les variantes associées, vous devez vous assurer que tous les niveaux graphiques sont définis exactement avec les mêmes paramètres afin qu'Unity ignore ces variantes.

Comme mentionné précédemment, Unity tentera de dédupliquer les variantes qui sont identiques, donc si, par exemple, deux des trois niveaux ont les mêmes paramètres, cela entraînera une réduction de la taille sur le disque, même si toutes les variantes seront toujours générées. Vous pouvez optionnellement forcer Unity à générer des variantes de niveau pour un shader donné et une API de rendu graphique, en utilisant les variants_hardware_tier comme indiqué ci-dessous :

// Direct3D 11/12
#pragma hardware_tier_variants d3d11 

Pour plus d'informations, consultez Les niveaux graphiques dans le pipeline de rendu intégré dans le manuel Unity.

Suppression basée sur les API graphiques

Unity compile un ensemble de variantes de shader pour chaque API graphique incluse dans votre build, donc dans certains cas, il est bénéfique de sélectionner manuellement les API et d'exclure celles dont vous n'avez pas besoin.

Pour ce faire, allez dans Paramètres du projet > Joueur. Par défaut, l'API graphique automatique est sélectionnée, et Unity inclura un ensemble d'API graphiques intégrées et en choisira une à l'exécution en fonction des capacités de l'appareil. Par exemple, sur Android, Unity essaiera d'utiliser Vulkan en premier, et si l'appareil ne le prend pas en charge, le moteur revient à GLES3.2, GLES3.1 ou GLES3.0 (les variantes seront identiques sur ces versions GLES).

Au lieu de cela, désactivez l'API graphique automatique pour la plateforme concernée, et sélectionnez manuellement les API que vous souhaitez inclure. Unity donnera alors la priorité à la première de la liste.

Désactivez l'API graphique automatique pour sélectionner vos API préférées

L'inconvénient est que vous pourriez limiter le nombre d'appareils qui prennent en charge votre jeu, donc assurez-vous de savoir ce que vous faites en changeant cela et testez sur une variété d'appareils.

Correspondance stricte des variantes de shader

Normalement, à l'exécution, Unity essaie de charger la variante qui est la plus proche de l'ensemble de mots-clés demandés si une correspondance exacte n'est pas disponible ou a été supprimée de la build du joueur. Bien que cela soit pratique, cela cache également des problèmes potentiels avec votre configuration de mots-clés de shader.

Depuis Unity 2022.3, vous pouvez sélectionner la correspondance stricte des variantes de shader dans Paramètres du projet > Joueur pour vous assurer qu'Unity essaie uniquement de charger la correspondance exacte pour la combinaison de mots-clés locaux et globaux dont vous avez besoin.

Activez la correspondance stricte des variantes de shader dans les paramètres du projet

Si non trouvé, il utilisera le shader d'erreur et imprimera une erreur dans la console contenant le shader, l'index du sous-shader, le passage réel et les mots-clés demandés. C'est assez pratique lorsque vous devez retrouver des variantes manquantes dont vous avez réellement besoin. Comme d'habitude avec le stripping, cela ne fonctionne que dans le Player et n'a aucun impact dans l'éditeur.

Exporter les variantes utilisées dans une collection de variantes de shader

En jouant au jeu dans l'éditeur, Unity garde une trace des shaders et des variantes actuellement utilisés dans votre scène et vous permet d'exporter cela dans a collection. Pour ce faire, naviguez vers Paramètres du projet > Graphiques. En bas, vous remarquerez une section de chargement de shader, montrant combien de shaders sont actuellement suivis comme actifs.

Assurez-vous de cliquer sur Effacer au préalable pour obtenir un échantillon plus précis, puis entrez en mode Lecture et interagissez avec votre scène, en vous assurant de rencontrer tous les éléments de jeu nécessitant des shaders spécifiques. Cela augmentera les compteurs suivis. Ensuite, appuyez sur le bouton « Enregistrer dans l'actif... » pour enregistrer tous ceux-ci dans un actif de collection.

Pour plus d'informations, consultez Créer une collection de variantes de shader dans le Manuel Unity.

Le bouton Enregistrer dans l'actif

Les collections de variantes de shader sont des actifs contenant une liste de shaders et de variantes associées. Elles sont couramment utilisées pour prédéfinir quelles variantes vous souhaitez inclure dans votre build et pour préchauffer les shaders.

Ajouter un shader à une collection de variantes de shader

Une approche utilisée dans certains projets consiste à exécuter cela pour chaque niveau du jeu, en enregistrant une collection pour chacun d'eux, puis en supprimant toutes les variantes qui ne sont présentes dans aucune de ces listes en utilisant un script IPreprocessShaders (couvert dans la section suivante). Bien que cela soit pratique, de mon expérience, c'est aussi assez sujet à des erreurs. Il est difficile de s'assurer que vous rencontrez toutes les variantes requises en une seule partie, et certaines des fonctionnalités peuvent n'être chargées que sur l'appareil et dans des cas spécifiques, ce qui entraîne une liste qui n'est pas nécessairement précise. À mesure que votre jeu évolue et que de nouveaux éléments sont ajoutés aux niveaux ou que les matériaux changent, les collections devront être mises à jour. Pour cette raison, je l'utiliserais principalement à des fins de débogage et d'investigation, plutôt que de l'intégrer directement dans votre pipeline de build.

Pour plus d'informations, consultez Créer une collection de variantes de shader dans le Manuel Unity.


Suppression des variantes de shader scriptables

Chaque fois qu'un shader est sur le point d'être compilé dans votre build de jeu, Unity déclenchera un rappel. Cela se produit à la fois sur les builds Player et Asset Bundles. Nous pouvons écouter cela facilement en utilisant IPreprocessShaders.OnProcessShader et IPreprocessComputeShaders.OnProcessComputeShader (pour les shaders de calcul), et ajouter une logique personnalisée pour supprimer les variantes inutiles. De cette manière, nous pouvons considérablement réduire le temps de build, la taille du build et le nombre total de variantes qui entrent dans votre build.

Pour ce faire, créez un script qui implémente l'interface IPreprocessShaders, puis écrivez votre logique de suppression dans OnProcessShader. Par exemple, voici un script qui supprimera toutes les variantes contenant le mot-clé de shader DEBUG lors des builds de production:

public class StripDebugVariantsPreprocessor : IPreprocessShaders
{
   public int callbackOrder => 0;

   ShaderKeyword keywordToStrip;

   public StripDebugVariantsPreprocessor()
   {
      keywordToStrip = new ShaderKeyword("DEBUG");
   }


   public void OnProcessShader(Shader shader, ShaderSnippetData snippet, IList<ShaderCompilerData> data)
   {
      if (EditorUserBuildSettings.development)
      {
         return;
      }

      for (int i = data.Count - 1; i >= 0; i--)
      {
         if (data[i].shaderKeywordSet.IsEnabled(keywordToStrip))
         {
            data.RemoveAt(i);
         }
      }
   }
}

L'ordre des rappels vous permet de définir quel script de prétraitement doit s'exécuter en premier, vous permettant de créer des passes de stripping en plusieurs étapes. Les scripts avec une priorité inférieure seront exécutés en premier.

Visitez le forum des Graphiques-Shader pour en savoir plus.

Plus de ressources

Pour plus d'informations, consultez les sections suivantes du Manuel Unity :