Blog de développeur | NXT - Lumières et éclairages dans Giélinor

Cette semaine, c'est au tour de nos vaillants développeurs graphistes de nous expliquer comment ils s'y sont pris pour améliorer les graphismes de 15 ans de contenus de RuneScape et faire en sorte qu'ils soient pris en charge sur un grand nombre de matériels, grâce à des techniques de rendu uniques bien connues de l'industrie ou toutes récentes.

Si vous êtes un graphiste en herbe ou passionné de technologies, ce blog devrait piquer votre curiosité !


Jusqu'à présent, dans nos blogs précédents, nous avions évoqué rapidement certaines des fantastiques fonctionnalités qui font du nouveau client de RuneScape un formidable moteur de jeu. Dans ce blog, nous allons les examiner plus en détail, mais aussi vous expliquer comment nous les avons implémentées et pourquoi nous les avons choisies.

L'un de nos plus grands défis pendant le développement de NXT, c'était d'améliorer la fidélité visuelle et les performances tout en nous assurant de conserver les graphismes de RuneScape qui vous sont chers. Voilà comment nous y sommes parvenus.

Illumination globale

L'illumination globale est la technique utilisée dans l'industrie des jeux vidéo et du cinéma pour modeler l'éclairage indirect d'une scène (c'est-à-dire la lumière réfléchie). Sans illumination globale, chaque pixel d'ombre apparaîtrait noir.

No Global Illumination

Sans illumination globale

C'est extrêmement difficile à implémenter en termes d'interactivité, et même aujourd'hui, la majorité des jeux vidéo utilisent un algorithme de prétraitement pour ancrer les éléments d'illumination globale dans des cartes de textures (ou textures d'éclairage) afin qu'ils soient accessibles plus rapidement à l'exécution.

Les jeux vidéo d'ancienne génération, par exemple Quake ou la série Half-Life, utilisaient la technique classique de radiosité pré-ancrée, mais plus récemment, les développeurs ont commencé à ancrer des données d'illumination globale plus détaillées dans leurs textures d'éclairage, comme les harmoniques sphériques et la visibilité sol-sol, car l'illumination globale présente l'avantage de fonctionner avec les textures d'éclairage normales.

Côté nouvelles technologies, on commence à voir émerger de vraies solutions en temps réel (volumes de propagation de la lumière et tracé de cônes par voxel), mais ces techniques ne sont pas encore tout à fait au point, et nécessitent une technologie de processeur graphique de pointe pour être efficaces.

Nous avions vraiment en tête d'améliorer cela avec le nouveau client, mais en raison de limitations techniques et de la taille conséquente de RuneScape, une solution d'illumination globale hors-ligne n'était pas viable. De plus, nous voulions développer un client qui soit pris en charge par tous nos matériels cibles.

Nous avons donc fouillé notre boîte à outils graphiques et opté pour une version moderne d'éclairage hémisphérique, qui utilise des textures d'environnement d'irradiance au moyen d'harmoniques sphériques.

L'éclairage hémisphérique s'appuie sur une sphère placée manuellement dans chaque environnement pour déterminer un dégradé de couleurs du ciel au sol. Les normales de surface de la géométrie de la scène sont ensuite utilisées pour sélectionner une couleur à partir de cette sphère. Par exemple, si une surface est orientée vers le ciel, la couleur au sommet de la sphère sera sélectionnée, et vice-versa.

Si nous avions demandé à des artistes de créer toutes ces sphères de lumière, cela nous aurait pris énormément de temps et de ressources. C'est pour cette raison que nous avons choisi une solution de programmation en temps réel.

Pour cela, nous avons placé une sonde de lumière (texture d'environnement globale) haut dans le ciel sur plusieurs images une fois chaque case de zone chargée, et ancré les harmoniques sphériques à partir de celles en temps réel, afin d'obtenir une texture d'environnement d'irradiance très compressée sous la forme de coefficients d'harmonique sphérique.

Nous avons ensuite entré ces coefficients dans le nuanceur de pixels, et ajouté la normale de surface afin de calculer l'irradiance du pixel en question. Nous avons ainsi obtenu un rayon de lumière réfléchie indirecte (lumière irradiante), ou comme nous l'appelons entre nous, un éclairage hémisphérique sous stéroïdes !

Nous avons aussi ajouté une occlusion ambiante dans notre mélangeur de lumière. L'occlusion ambiante simule des ombres douces, à petite échelle, émises par l'éclairage de l'environnement et déterminées par le degré de visibilité de la surface. Nous avons donc choisi une forme d'occlusion de type Screen Space Ambient Occlusion (occlusion ambiante dans l'espace de l'écran, ou SSAO), appelée Horizon-Based Ambient Occlusion (occlusion ambiante fondée sur l'horizon, ou HBAO), qui est actuellement la meilleure sur le marché.

Cependant, contrairement à la plupart des jeux qui appliquent le SSAO en post-traitement (ce qui donne des résultats plutôt médiocres et peu convaincants), nous appliquons notre occlusion ambiante pendant la passe d'éclairage avancé et ce uniquement à la lumière ambiante indirecte. Par conséquent, les pixels qui sont éclairés directement ne présenteront pas une occlusion ambiante trop élevée, ce qui correspond mieux à la réalité. Nous effectuons également une passe de SSAO en haute résolution, pour des résultats plus stables.

Les captures d'écran ci-dessous illustrent nos solutions d'irradiance et d'occlusion ambiante combinées. Nous espérons que vous trouverez vous aussi que le résultat constitue une nette amélioration par rapport au client Java actuel.

Java Global Illumination

Illumination globale (Java)

NXT Global Illumination

Illumination globale (nouveau client NXT)

Java Global Illumination

Illumination globale (Java)

NXT Global Illumination

Illumination globale (nouveau client NXT)

Rendu à plage dynamique élevé (High Dynamic-range Rendering, ou HDR), correction gamma et mappage tonal

Si les graphismes manquent de relief et semblent sursaturés dans Java, c'est parce que le client ne peut pas afficher la gamme complète des couleurs et des intensités lumineuses de la scène. Pour remédier à cela, nous devons d'abord nous assurer que le processeur graphique calcule l'éclairage dans un espace linéaire.

Tous les processeurs graphiques effectuent leurs calculs à l'aide d'unités de calcul en virgule flottante, mais pour en tirer pleinement parti, les données des équations d'éclairage pendant l'exécution du nuanceur doivent elles aussi se trouver dans un espace linéaire. C'est pour cela que nous devons nous assurer de convertir les textures du jeu, stockées dans l'espace sRGB depuis Photoshop, dans un espace linéaire pour qu'elles puissent être utilisées par les nuanceurs.

Ce même processus est également appliqué à d'autres éléments définis par les artistes, comme les couleurs de la lumière et du brouillard. La plupart des processeurs graphiques peuvent effectuer cette conversion au niveau matériel, mais lorsque c'est impossible, nous disposons d'une technique pour l'effectuer manuellement. Cette conversion nous permet de garantir que le rendu à plage dynamique élevé de l'éclairage ne soit pas pollué par des éléments non linéaires et que l'éclairage de la scène soit régulier, mais aussi d'éviter que des zones fortement éclairées ne s'effacent, ce qui pourrait détruire les détails de lumière et de texture.

Pour parvenir à un rendu à plage dynamique élevé total, nous devons aussi stocker les résultats de ces calculs d'éclairage dans l'espace linéaire dans des textures hors écran qui préserveront la linéarité en utilisant des formats à virgule flottante. Toutefois, comme ces textures à virgule flottante peuvent être coûteuses en ressources, nous essayons généralement d'utiliser un format à virgule fixe.

L'élément le plus fascinant du rendu à plage dynamique élevé est sans aucun doute le mappage tonal, qui permet de mapper une gamme de couleur à une autre. Dans notre cas, cela implique de convertir les résultats d'éclairage linéaires du rendu à plage dynamique élevé en une gamme prise en charge par les écrans, qui ne peuvent afficher que les valeurs à faible gamme dynamique (LDR).

Sans mappage tonal, la conversion d'images HDR en images LDR entraînerait une grande perte de données d'éclairage, et l'image obtenue ne serait pas belle. Il nous a donc fallu travailler d'arrache-pied avec les artistes pour évaluer les différentes formules de mappage tonal et obtenir un résultat qui correspondrait au style actuel de RuneScape, tout en conservant une gamme dynamique des couleurs et des intensités lumineuses adéquate. C'est pour cette raison que nous avons choisi un mappage tonal filmique.

Ombres en temps réel

Dans la plupart des jeux capables de calculer leur illumination globale hors ligne, les ombres émises par la géométrie de la scène statique à partir de sources de lumière dominantes (comme la lumière du soleil) ne sont généralement pas incluses dans le processus d'ancrage de la lumière (les ombres font partie de l'illumination globale, mais là n'est pas le sujet). Là encore, comme cette option n'était pas viable pour nous, nous avons choisi une solution entièrement dynamique. Le défi lorsqu'on développe un système d'ombres en temps réel intégralement dynamique, c'est de réussir à atteindre un haut niveau de qualité et de performance. En termes de qualité et de performance, la meilleure technique de rendu d'ombres en temps réel sur les processeurs graphiques modernes s'appuie sur le mappage d'ombres. Cependant, l'algorithme de mappage d'ombres pose deux problèmes principaux : le crénelage de projection et le crénelage de perspective, liés à une faible résolution du mappage d'ombres. Si la résolution est insuffisante, de nombreux texels de texture d'ombres seront mappés à un seul pixel, entraînant ainsi une série d'artefacts de crénelage.

Crénelage Sans crénelage

Nous avons opté pour un modèle de Parallel-Split Shadow Map (texture d'ombres en cascade par plans parallèles, ou PSSM), qui sépare la scène visible en segments, chacun se rapportant à une cascade d'ombres unique, pour obtenir un meilleur ratio de texels d'ombre par pixel d'espace à l'écran. Cette méthode améliore considérablement le crénelage de perspective, qui serait visible sans cela si nous utilisions une seule texture d'ombre pour la scène toute entière. L'inconvénient cependant, c'est que la scène doit être rendue pour chaque cascade d'ombres, ce qui augmente fortement le nombre d'objets de chaque draw call (objet dessiné à l'écran), et peut gravement ralentir les performances du client.

Nous avons donc dû travailler sur plusieurs fronts pour réduire cette explosion de draw calls. Plusieurs niveaux d'élimination de visibilité sont appliqués à chaque passe de rendu d'ombres en cascade, notamment le view frustum culling (élimination des objets hors du cône de vue), le distance-based culling (élimination des objets en fonction de la distance), le shadow map area culling (élimination des objets de zone de texture d'ombre) et le shadow caster volume culling (élimination des volumes projetant de l'ombre). De plus, nous pouvons éliminer davantage d'objets en nous appuyant sur diverses hypothèses relatives à la scène. Par exemple, nous ne rendons pas les irrégularités de terrain peu profondes dans les textures d'ombre car elles ne projettent par d'ombre sur la scène, et nous ne mettons à jour que les ombres en cascade éloignées sur les images alternées afin de réduire le nombre moyen de draw calls.

De plus, pour réduire encore plus le problème de crénelage de texture d'ombre, dans le cas des ombres en cascade éloignées qui couvrent une grande partie de la scène visible, nous utilisons un algorithme appelé unit cube clipping (détourage des cubes) afin que la projection orthographique des textures d'ombre correspondent parfaitement aux ombres portées et aux éléments émetteurs d'ombre. Cet algorithme améliore considérablement l'utilisation des textures d'ombre dans de nombreuses scènes.

Nous devons nous assurer que chaque draw call de texture d'ombre soit le moins coûteux possible. Pour y parvenir, nous avons recours à plusieurs « trucs » : nuanceurs de sommets personnalisés, nuanceurs de pixels et formats de sommets minimum. Grâce à ces astuces, l'utilisation du processeur graphique reste minimale lors du processus de génération des textures d'ombre.

Dernier élément primordial pour s'assurer de la qualité et des performances de toute technique de texture d'ombre : le filtrage utilisé pour produire des arêtes d'ombre douces. L'utilisation du filtrage de textures d'ombre par la technique de filtrage Percentage Closer Filtering (PCF) du processeur graphique nous permet d'obtenir des échantillons de textures d'ombre interpolés. Grâce à cette technique, combinée à des noyaux de filtrage et des fonctions de recherche de textures permettant de réduire l'utilisation des registres généraux, nous sommes parvenus à obtenir des ombres à arêtes douces tout en conservant des performances élevées.

Si vous êtes encore avec nous à ce stade, vous avez certainement compris à quel point le rendu de texture d'ombre est complexe, mais le résultat en vaut vraiment la peine !

Interaction des ombres

Light Indexed Deferred Lighting

La lumière est très présente dans les environnements de RuneScape ! Le nouveau client calcule toutes les lumières par pixel sans aucun ancrage, ce qui signifie que tous les éclairages sont considérés comme dynamiques. L'ombrage différé constitue l'une des approches modernes pour les scènes à éclairage dynamique. Cependant, cette méthode présente aussi des inconvénients, et n'est pas viable tant que nous devons encore prendre en charge des matériels aux spécifications basses.

En conservant notre pipeline de rendu d'éclairage, l'approche standard d'éclairage utilisant 8 à 16 sources de lumière par objet n'était pas suffisante en raison de la taille des lots de géométrie. Nous devions donc trouver une autre solution !

Nous avons opté pour une technique appelée Light Indexed Deferred Lighting, qui se situe à mi-chemin entre la technique d'éclairage différé et celle de l'éclairage avancé. Elle nous permet de prendre en charge jusqu'à quatre lumières uniques par pixel, et s'adapte parfaitement à notre pipeline de rendu d'éclairage avancé. Nous avons ainsi résolu l'ensemble de nos problèmes d'éclairage pour les grands lots de géométrie statique, tout en continuant de prendre en charge l'anti-crénelage MSAA (Multisample Anti-Aliasing, anti-crénelage multi-échantillons) et différentes formules d'éclairage pour les prochaines mises à jour matérielles.

Many Point Lights

Nombreux points d'éclairage

Effets de l'eau

Dispersion de l'éclairage atmosphérique

Compte tenu des distances d'affichage plus élevées, nous savions que le brouillard de distance de Java ne suffirait plus. Pour commencer, nous avons supprimé complètement le brouillard de distance pour le remplacer par une technique de dispersion d'éclairage atmosphérique. Le résultat était réussi, mais ne parvenait pas à masquer complètement la limite du monde. Nous avons donc décidé de combiner notre brouillard de distance d'origine avec notre nouvelle technique de dispersion atmosphérique afin de créer une solution hybride qui viendrait compléter le brouillard Java. Nous avons ainsi obtenu un résultat final beaucoup plus naturel, surtout dans le cas des distances d'affichage plus élevées, ainsi qu'une meilleure perception de la profondeur de la scène.

Rendu de l'eau

L'une des principales caractéristiques de notre nouveau système est sans aucun doute le rendu d'eau. Comme pour le reste, nous sommes repartis de zéro pour le concevoir, mais nous avons aussi pris l'audacieuse décision de revenir à notre ancien jeu de données d'eau Java, afin de résoudre les différents bugs encore présents et dont nous avions hérités d'HTML5. Nous utilisons encore les données de patchs pour les réflexions planaires en temps réel, mais la géométrie de l'eau en elle-même a été reconstruite à partir du jeu de données Java. Nos artistes n'ont pas eu besoin de refaire tous les patchs, ce qui constitue un gain de temps considérable.

Le nuanceur lui-même est composé de multiples éléments afin de rendre l'apparence souhaitée. Tout système de rendu d'eau s'appuie sur deux composants principaux : la réflexion et la réfraction en temps réel. Ces deux éléments étaient donc incontournables. Par ailleurs, nous nous sommes assurés que l'éclairage sur l'eau interagisse correctement avec notre système d'ombre. La couleur spéculaire directe est donc masquée correctement lorsque des ombres sont projetées. Nous avons également amélioré les effets de vague en modifiant l'échantillonnage des textures d'eau normales dans les zones où il n'y a pas ou peu de déformations, ce qui réduit grandement les artefacts de case qu'on retrouve souvent dans les effets d'eau. Enfin, grâce à notre ancien jeu de données, nous avons accès à de meilleures informations sur les terrains immergés, ce qui nous a permis d'atténuer différents composants, comme le brouillard ou les effets de déformation des vagues lorsqu'elles approchent du rivage.

Les résultats sont plutôt fabuleux, non ?

Effets de l'eau Effets de l'eau Effets de l'eau

Techniques de réduction des draw calls

Il ne s'agit pas d'une particularité à proprement parler, mais nous aurions tort d'oublier l'un des principaux éléments grâce auquel nous avons pu obtenir de meilleures performances qu'avec Java et un rendu bien supérieur. J'ai déjà évoqué rapidement comment nous sommes parvenus à réduire nettement les draw calls pour le rendu des textures d'ombre, mais ce qui a beaucoup joué, c'est la réduction des draw calls pour les passes d'éclairage avancé et de profondeur de scène.

La procédure la plus coûteuse en termes de temps processeur pour un moteur de jeu, c'est de soumettre les objets à dessiner à l'écran. Ce coût peut être divisé en deux : d'abord, le coût du traitement par le pilote de graphiques du processeur des draw calls lors de la mise en place de tampons de commandes du processeur graphique, et ensuite, le coût du processeur graphique impliqué dans le traitement des sommets et des pixels.

J'ai déjà parlé de l'élimination des objets hors du cône de vue (view frustum culling) et de l'élimination des objets en fonction de la distance (distance-based culling), mais comme les draw calls de la passe d'éclairage avancé sont beaucoup plus coûteux que ceux de la passe d'ombrage simple, nous avons dû aller plus loin.

Le meilleur moyen de réduire les coûts des draw calls, c'est d'utiliser notre système de lots de géométrie dynamique. Lors du chargement des objets, ceux-ci sont regroupés en fonction de leur matériau, puis rattachés ensemble afin de pouvoir être rendus en un seul draw call. L'inconvénient de cette technique, c'est qu'elle nécessite un système d'atlas de texture complexe qui permette que toutes les textures des objets groupés en lots soient accessibles à partir d'une seule page de texture. Cela a pour conséquence de déformer la taille des sommets du modèle en raison des informations supplémentaires requises pour accéder à la texture d'un objet dans un atlas, mais aussi d'augmenter le nombre d'instructions du nuanceur de pixels. Cependant, ce coût additionnel est relativement faible comparé au gain net obtenu en termes de performance grâce à la réduction des draw calls par traitement par lot. Ce même système de traitement par lot est également utilisé par toutes les passes de rendu afin de réduire davantage les draw calls.

Draw call batches

Chaque couleur indique un lot de draw calls

Dernière pièce du puzzle de réduction des draw calls : notre système unique et innovant d'élimination des objets cachés. Contrairement à la plupart des solutions d'élimination des objets cachés qui nécessitent un traitement de la scène hors ligne afin de générer des ensembles potentiellement visibles en des points donnés du monde, ou à une géométrie d'occlusion pour des solutions en temps réel, notre solution ne nécessite aucun de ces éléments. En effet, en raison de la limitation de nos ressources artistiques, il n'était pas possible de traiter un grand nombre d'environnements de RuneScape hors ligne, ni de générer une géométrie d'occlusion par proxy pour une solution hors ligne. Nous avons donc développé une technique hybride, qui s'appuie sur un rastériseur logiciel côté processeur pour traiter les requêtes d'occlusion, mais au lieu de générer des données de profondeur de la scène côté processeur, nous transférons les données de tampon de profondeur de la scène générées à partir des images précédentes pour alimenter nos requêtes d'occlusion côté processeur. Tous ces éléments ont un coût fixe élevé, mais dans des scènes caractérisées par des profondeurs complexes, la réduction des draw calls par occlusion nous a permis d'améliorer considérablement les performances, notamment sur les ordinateurs équipés de pilotes de graphiques médiocres.

C'est tout pour moi !

J'espère que ce blog vous aura intéressé et permis de comprendre combien il nous a été difficile de concevoir un nouveau client plus performant et plus beau visuellement que le client Java avec des contenus vieux de 15 ans.

Nous pensons que les décisions prises pendant ce projet serviront de fondations solides aux futurs projets RuneScape, pour vous offrir des performances graphiques améliorées dans les années à venir.

Mod Lordgit
Programmeur graphiste en chef

Haut de la page