Optimisations

Il est vite devenu indispensable d'optimiser certaines parties du projet. Les parties critiques (partie temps réel de rendu) ayant déjà été optimisées grâce à la gestion des niveaux de détail, seuls le frustum culling et l'occlusion map y ont été ajoutés. Nous avons surtout tenté d'accélérer la phase de chargement et de pré-calcul. Ces différentes optimisations sont présentées ci-dessous.

Optimisations des précalculs

Maillage

Après profilage de l'application, il s'est avéré qu'une très grande partie du temps de chargement était due à l'utilisation de vector pour le stockage des vertices. En particulier, les fonctions d'itération et de récupération du tableau de vertices paraissaient très lourdes. La solution a donc été de remplacer les vector pour le maillage par des simples tableaux. En plus d'une lisibilité accrue, le gain a été évident (de l'ordre de 30% de gain). Malgré ces performances bien meilleures, des vector ont été conservés lorsqu'ils sont de petite taille, ou bien que la taille des données n'est pas connue par avance, comme par exemple dans le cas du chargement des heightmaps.

Géomorphing

La deuxième partie majeure dans le chargement de l'application est le calcul des données de géomorphing. Le code est séparé en deux parties :

  1. une première fonction (computeGeomorphingVertices), qui itère à travers le maillage triangulé pour déterminer quels points calculer ;
  2. une deuxième fonction (computeGeomorphingVertex), appelée par la première fonction pour chaque point à calculer.

La première fonction étant principalement constituée de boucles et de très peu de calcul, elle n'était probablement que très peu optimisable. Nous avons donc concentré nos efforts sur la deuxième fonction, qui a été entièrement remaniée pour éviter au maximum les conversions d'entiers à flottants et inversement, qui étaient clairement pour une grosse part dans la performance de la fonction. Le nombre d'appels de la fonction étant de l'ordre de plusieurs millions, le gain a été immédiat.

Lightmap

Enfin, la dernière partie du chargement à être assez longue est le calcul de la lightmap. Celle-ci pourrait être optimisable en calculant toutes les normales pour tout le maillage, en les stockant temporairement, puis en les utilisant pour calculer les moyennes. Chaque normale ne serait alors calculée qu'une seule fois, alors que pour l'instant on calcule pour chaque vertex les 6 normales nécessaires, même si elles ont déjà été calculées pour un autre vertex. Cette optimisation n'a cependant pas été implémentée pour l'instant.

Frustum Culling

Pour optimiser la partie critique de l'application, à savoir le rendu temps réel, nous avons implémenté le frustum culling. Cette méthode consiste à utiliser des boîtes englobantes pour chaque patch pour déterminer s'il se trouve ou non dans le frustum de vue. Si le patch est totalement en dehors du frustum de vue, alors il est garanti (à quelques rares exceptions près) qu'il ne sera pas visible, et n'est donc pas dessiné. Dans le cas contraire, il est dessiné, même si la partie de la boîte englobante qui est dans le frustum de vue est en fait vide de tout objet. Grâce à cette optimisation, la vitesse de rendu a été multipliée en moyenne par 2.

Suppression des parties cachées (Occlusion Maps)

La motivation de cette partie vient du coté montagneux du relief que nous générons. En effet, nous avons vite constaté que lorsque nous nous trouvions aux milieux de vallées ou face à des montagnes, une grande partie du terrain que nous affichions n'était pas visible. Afin de gagner en vitesse et en fluidité nous avons donc choisi d'implémenter un algorithme permettant de détecter et d'ignorer ces patchs, au lieu de les afficher pour rien comme nous le faisions au début.

L'algorithme devait permettre de détecter les patchs invisibles rapidement afin qu'il soit réellement utile. Effectivement, s'il est plus long de détecter les patchs à ignorer que de les afficher, il n'y a pas d'intérêt. C'est pour cela que nous avons choisi d'employer l'algorithme nommé Occlusion Map (Carte d'occlusion).

Cet algorithme consiste à précalculer une carte d'occlusion pour chaque patch du terrain. Chacune de ces cartes a pour dimension le nombre de patch du terrain. Ainsi, cette carte associe une hauteur pour chaque patchs, celle-ci détermine la hauteur minimale où la caméra doit se trouver pour que le patch soit visible si on le regarde à partir du patch pour lequel la carte d'occlusion est calculée.

Une fois que toutes les cartes d'occlusions sont calculées, il suffit de donner au programme la position de la caméra et la position du patch dont on souhaite tester la visibilité. Ensuite le programme récupère la carte d'occlusion correspondant à la position de la caméra et teste si la hauteur est supérieure à la valeur de la carte pour le patch donné auquel cas il est affiché ou ignoré dans le cas contraire.

Carte d'elevation Carte d'occlusion 1 Carte d'occlusion 2
Carte d'élévation avec deux exemples de carte d'occlusion
(calculées pour les patch marqués par les points vert et rouge)

Gestion mémoire

Lors de la phase d'optimisation, il s'est avéré que la plupart de la mémoire allouée n'était pas libérée. Après analyse de ces fuites mémoire, nous avons réussi à réduire les plus importantes. La mémoire non désallouée est ainsi passée de plusieurs centaines de mégaoctets à seulement quelques dizaines. Bien entendu il sera nécessaire de poursuivre dans cette voie...