Mise en oeuvre



 

Gérer des images avec l'API java

La caméra
La caméra est modélisée dans une classe à part entière afin d'encapsuler tous les calculs de position et de déplacement. Cette encapsulation rend le programme plus clair, et permettra de laisser plus de liberté pour des extensions ultérieures de la caméra (déplacement défini en temps réel par l'utilisateur).
Les paramètres de la caméra sont répartis en quatre groupe :
  •  
  •  

  •  
  • Les calculs de position suivent les formules trigonométriques suivantes :





     
     

    --------------------------------- Retour à la liste des problèmes ---------------------------------

    Le raysurfing

     
    1. Principe

    Le raysurfing est en fait une technique de lancer de rayon (raytracing) spécialisée. En effet, pour calculer une image, le raytracing impliquera de lancer un rayon par pixel alors que le raysurfing nécessitera de lancer un rayon par colonne de pixels, moyennant des objets de formes adaptées (nous y reviendrons plus tard en voyant les limitations de cette technique).

    2. Voxels et heightmap

    Pour comprendre la base de cet algorithme, il nous faut introduire la notion de voxel : de même qu'un pixel est un carré élémentaire en 2D, un voxel est un parallélépipède en 3D. Les voxels sont très employés pour la représentation de paysages pour une principale raison : la simplicté : nous aurons à disposition une carte des hauteurs indiquant pour un voxel donné (aux coordonnées (X,Z) ) sa hauteur Y. L'information peut donc être stockée dans une simple image bitmap 256 couleurs !

    3. Du raytracing au raysurfing

    Passons maintenant à ce qui constitue la restriction du raytracing pour obtenir le raysurfing. Pour le raytracing, nous devons trouver tout d'abord l'équation du rayon passant par le pixel de l'écran dont nous voulons calculer la couleur puis ensuite se déplacer le long de ce rayon afin de trouver l'intersection avec le plan. Cependant, cette méthode est extrêmement lente : nous devons calculer ces informations pour chaque pixel de l'écran. L'idée du raysurfing est de calculer seulement une équation de rayon au départ puis d'en déduire celles pour les rayons partant des pixels de la même colonne si on trouve l'intersection avec le paysage. Ensuite, repartir du point d'intersection avec l'équation du nouveau rayon déduite de la précédente et continuer jusqu'à une certaine condition d'arrêt. Un des gros avantages de cette méthode est que les parties non visibles du paysage ne sont pas calculées, épargnant ainsi le calcul des zones visibles.
    Pour cela, nous avons besoin de plusieurs restrictions : tout d'abord, les paysages ne pourront pas contenir de zones surplombantes. Ensuite, la caméra devra toujours avoir son axe tangent parallèle au paysage (les rotations sont autorisées seulement selon les axes Y et X).

    4. Un algorithme

    Nous pouvons donc maintenant obtenir l'agorithme, pour une colonne de pixels :
     

    Trouver les incréments servant à se déplacer sur le rayon (dx, dy, dz)
    Tant que l'on a pas atteind la distance de vision maximale
    Récupérer l'altitude du point situé à la verticale du point courant du rayon
    Tant que le point courant se situe en dessous du niveau du paysage
    Appliquer la bonne couleur au point sur l'écran
    Remonter d'un pixel sur l'écran
    Augmenter l'ordonnée du point courant du rayon de 1
    Fin tant que
    Recalculer dy
    Mettre à jour le point courant du rayon
    Fin tant que

    Il suffit alors de répéter ceci sur chaque colonne de pixels.
    Pour ensuite appliquer une texture à l'ensemble, nous effectuons un texturage plannaire, et nous prenons donc les coordonnées X et Z du point courant sur le rayon comme coordonnées de texel.

    Tracé du ciel

    Deux méthodes de tracé du ciel ont été testées : par raytracing naïf puis par raysurfing, afin d'optimiser la vitesse de rendu.

     

    1. Raytracing naïf

    Tout d'abord, considérons l'équation d'un plan horizontal infini (normal à la direction y) : elle sera de la forme y = constante. Partant de ce principe, il n'est pas difficile de revenir aux coordonnées d'intersection du plan étant donné l'angle de départ du rayon :





    soient alpha et bêta les angles de départ du rayon selon respectivement la direction tangente à la camera et l'axe y. On aura donc :




    Étant donné que nous allons effectuer un mapping planaire, nous utilisons directement ces coordonnées comme coordonnées dans la texture, après les avoir ramené à la taille de celle-ci par modulo. De plus, dans l'implémentation finale, pour simuler le fait que les nuages ne peuvent être atteints, nous ne prenons pas en compte l'altitude de la caméra, ce qui donne comme formule :


    Cependant, la présence de nombreuses fonctions trigonométriques pénalise la vitesse de cette méthode. Nous allons donc revenir au raycasting, étant donné que le plan respecte les restrictions inhérentes à la spécialisation des rayons.

    2. Raysurfing

    L'algorithme du raysurfing reprend exactement celui cité plus haut. Seulement, au lieu d'avoir à lire une hauteur dans la heightmap, nous raisonnons par rapport à une constante. De plus, en cas de collision, nous faisons varier l'altitude de la position sur le rayon de -1 au lieu de 1 (pour se retrouver en dessous du ciel, et non au dessus).

    3. Résultat

    Le brouillard
     
    Le brouillard est un des effets donnant le meilleur rapport difficulté d'implémentation / amélioration du rendu final. En effet, le fait de voir des montagnes s'arrêter net, sans suite au paysage est très désagréable, surtout quand le paysage est animé. Pour remédier à cela, il suffit d'ajouter aux couleurs du pixel que l'on veut écrire, une certaine quantité de blanc, cette quantité dépendant naturellement de la distance à la caméra. Nous prendrons ainsi deux distances caractéristiques de la caméra : la distance de vision maximale qui donnera un coefficient de 1 et la distance de début du brouillard où le coefficient sera de 0. Pour vous convaincre de l'utilité de cet effet, voici deux captures d'écran.


    Vue sans brouillard


    Vue avec brouillard

    L'eau
     

    1. La réflexion

     

    La mer est un plan horizontal défini par une altitude. Dès lors, la modélisation de la surface de la mer est beaucoup plus simple que celle des montagnes. Les points à traiter en tant que surface de la mer sont les points dont l’altitude (coordonnée Y) est égale au niveau de la mer. Une fois repérés, ces points devront être l’origine d’un nouveau rayon.

    D’après les lois de réflexion de la lumière, le rayon incident et le rayon réfléchi doivent avoir un angle opposé par rapport à la normale au plan de projection. En d’autres termes et dans notre repère, nous devons lancer un nouveau rayon avec le même angle Ry et un angle Rxz opposé :

     

    2. Les vagues

    Jusque là, notre eau est une mer d’huile, un vrai miroir. Pour donner un aspect visuel plus réaliste, la mer doit être agitée par le vent : c’est la naissance des vagues. Visuellement, la représentation des vagues se traduit par une légère variation périodique de l’angle de réflexion, selon les deux axes du plan.

     
    Remarques :
    . On négligera la variation du niveau de la mer (hauteur des vagues). Vu l’altitude de la caméra, la variation d’altitude de la mer n’est pas visible.
    . En réalité, la variation de l’angle de réflexion porte sur les angles Ry et Rxz. Toutefois la variation de l’angle Ry est négligeable. On ne prendra en compte alors que Rxz. :
     
    --------------------------------- Retour à la liste des problèmes ---------------------------------
    L'effet Lens Flare (effet de lentille)
     
     
    1. Qu'est ce que l'effet Lens Flare

    L’effet de lentille affiche à l’écran une série de halo lumineux quand une source lumineuse intense est dans le champ de vision. Dans la réalité, cet effet est du aux différentes lentilles de l’objectif d’une caméra ou d’un appareil photo.


    2. Comment afficher un halo lumineux ?

    Un halo est l’éclaircissement d’une zone (généralement un disque) de l’image. Le halo doit donc être appliqué sur l’image après que celle-ci ait été calculée (i.e. tous les rayons aient été lancés).

    En fonction des paramètres suivants on détermine d’abord si le soleil est dans le champ de vision de la caméra, puis si c’est le cas les coordonnées de projection  du soleil sur l’écran :

    - la position du soleil dans le ciel (une direction, i.e. dans notre repère un couple d’angle (Ry,Rxz))
    - l’orientation de la caméra (les angles Ry et Rxz de la caméra)
    - la champ de vision de la caméra (un angle d’ouverture calculé à partir de la distance de projection FOV et des dimensions de l’écran de projection dimX et dimY)

    Puis on place 4 halos lumineux sur la droite passant par le soleil et le centre de l’écran. On applique des coefficients barycentriques constants pour la position de chaque halo (les coefficients sont déterminés de manières arbitraires, ils dépendent de la position de chaque lentille dans l’objectif, nous ne rentrerons pas à ce niveau de précision !).

    Puis pour l’affichage, nous réutilisons la classe ReadableImage implémentée pour la lecture d’une heightmap. Pour l’affichage d’un halo, on calque une ReadableImage sur l’image calculée.

    On dispose de « tâches » en niveaux de gris (fichiers .gif). Pour chaque halo, on « posera » une tâche (notre calque) sur notre paysage : chaque pixel de la tâche est associé à un pixel du paysage. Puis on teste pour chaque pixel de la tâche si le pixel en question dépasse un seuil (un niveau de gris déterminé eu préalable). Si le pixel est plus clair que le seuil fixé, on éclaircit le pixel du paysage correspondant, en fonction de la marge de dépassement du seuil : plus le pixel du calque tend vers le blanc, plus le paysage est éclairci.



     
    3. Le résultat

    Le résultat final avec quatre halos :



     

    --------------------------------- Retour à la liste des problèmes ---------------------------------


    Optimisation 1 : l'interpolation bilinéaire
     
    1. Principe

    Pour effectuer un raytracing ou un raysurfing, nous devons passer par chaque pixel de l'écran afin de trouver la couleur qui lui correspond. Souvent, nous ne nécessitons que le calcul des coordonnées du texel correspondant au pixel de l'écran puis affecter sa couleur à celle du point à afficher. Une première idée serait donc de lancer un rayon à intervalles réguliers (un sur 2, 4 ou 8 par exemple) puis d'effectuer une interpolation des couleurs pour déterminer celle des pixels intermédiaires. Cependant, il en résultera une impression d'image floue donnant envie d'essayer de nettoyer son écran. L'idée suivante consiste donc à stocker non pas la couleur mais les coordonnées du texel puis d'effectuer l'interpolation sur ces coordonnées.
    Pourquoi le terme bilinéaire ? Et bien tout simplement car nous sommes en dimension 2 : nous aurons à effectuer une interpolation suivant deux axes et ce, linéairement.

    2. Les formules


    soit un carré de coté n

    La formule pour trouver les coordonnées du texel correspondant à P est :

    L'algorithme naïf de l'interpolation bilinéaire serait d'appliquer cette formule à chaque pixel de l'écran. Néanmoins, aux vues du nombre de multiplications qu'elle contient, nous perdrions beaucoup de temps, ce qui est gênant pour une technique sensée accélérer le programme. Remarquons que nous avons cité le terme linéaire ; ceci signifie que les quantités varient de manière plus ou moins constante selon x et y. Nous pouvons donc calculer les dérivées partielles correspondant à la formule :






    De plus, nous pouvons remarquer que ces quantités ne sont pas constantes : elles dépendent chacune soit de x, soit de y. Étant donné que nous raisonnons d'abord par ligne puis par colonne, nous allons calculer la variation de dv / dx quand nous changeons de ligne.

    3. L'algorithme

    Pour une ligne sur n de l'écran

    Pour un point sur n de cette ligne P
    calculer dv / dx en P.y = 0
    calculer dv / dy en P.x = 0
    calculer d²v / dxdy

    valeurAgauche = P1

    Pour j allant de P.y à P.y + n

    texelCourant = valeurAgauche

    Pour i allant de P.x à P.x + n

    affecter au pixel de l'écran la couleur du texel courant
    TexelCourant += dv / dx
    Fin pour

    dv / dx += d²v / dxdy
    valeurAgauche += dv / dy

    Fin pour
    Fin pour
    Fin pour

    4. Résultats et conclusion

    Je pense qu'une image parle plus que des mots. Voici une capture d'écran de l'application mettant en oeuvre cette optimisation avec n = 8, soit le lancer de un rayon sur 64 !

    Encerclés en rouge, des exemples d'erreurs effectuées à cause de l'interpolation linéaire.

    Comme nous pouvons le voir, les flancs des montagnes sont rendus correctement, cependant, des problèmes apparaissent lors des franchissements de colline. Ceci est du au fait que nous ne pouvons pas considérer un paysage comme un espace linéaire : la distance entre deux rayons peut varier de manière très importante ou très faible selon que l'on change de colline ou non. Cette technique d'interpolation ne peut donc pas être employée de manière satisfaisante pour le tracé de nos paysages.
    Néanmoins, cette technique peut être utilisée dans le rendu du ciel (voir le dossier de maintenance, au chapitre améliorations)

     
     

    --------------------------------- Retour à la liste des problèmes ---------------------------------

     

    Optimisation 2 : changement du niveau de détail selon la distance
     
    1. Principe

    Un constat sur le calcul de l'image nous a amené à remarquer que nous utilisions la même précision pour ce qui concerne l'avant plan et le lointain. Hors, pour les éléments de paysage situés dans la distance, le fait que l'image soit un peu moins précise n'est pas particulièrement gênant aux vues des gains en vitesse que cela pourrait nous apporter.

    2. Deux approches différentes

    Nous avons imaginé deux principales solutions pour adapter le niveau de détail : la première consiste à changer la vitesse à laquelle nous nous déplaçons sur le rayon en fonction de la distance. La seconde est d'utiliser des distances de vision différentes selon les rayons puis d'en déduire les informations manquantes. Nous avons choisi d'implémenter la seconde méthode car la première pourrait conduire à des erreurs conséquentes sur la représentation du paysage. En effet, en augmentant l'incrément, nous risquons de complètement sauter une montagne si celui ci est trop grand. Cependant, cette méthode peut être utilisée en combinaison avec la seconde en utilisant des tailles d'incrément raisonnables.

    3. Explication de la seconde méthode

    Le principe de cette méthode sera de faire varier régulièrement la distance maximale à laquelle pourront être lancés les rayons (voir le schéma explicatif ci-dessous).

    Pour un rayon sur 4, nous lancerons à distance maximale. Pour le rayon suivant, à une distance minimale, pour le 3e, à une distance intermédiaire et enfin un 4e à une distance minimale. Il va en résulter que nous devrons retrouver les informations manquantes pour les rayons lancés à une distance minimale ou intermédiaire. Comme nous savons que de toute façon nous ne nous déplaçons sur l'écran que vers le haut, nous pouvons dire que des informations manquent si le voisin d'un rayon de distance minimale ou intermédiaire a été projeté jusqu'à une position plus haute sur l'écran. Dans un tel cas, nous devrons déduire les informations de celles contenues dans les colonnes correspondant aux rayons ayant été lancés à une distance plus grande. Dans le cas de notre adaptation à trois distances différentes, nous aurons au loin des blocs de pixels de largeur 4 puisque les informations pour les rayons 2, 3 et 4 seront déduites de celles du premier rayon.

    4. Modifications de l'algorithme de lancé de rayon

    Les adaptations à faire pour modifier l'algorithme précédent ne sont pas très conséquentes : il faut tout d'abord ne pas lancer les rayons successivement, mais par ordre de distance de vision maximale : tous les rayons de distance maximale, puis les rayons de distance intermédiaire et enfin ceux de distance minimale. Ensuite, il nous faudra une structure de donnée nous permettant de stocker les différentes hauteurs atteintes par les rayons 1 et 3 afin de détecter la perte d'information comme expliqué dans le 3. Concernant le lancer du rayon proprement dit, nous lançons le rayon

    5. Résultat

    Zone 1 : distance de vision maximale - paysage très pixelisé.
    Zone 2 : distance intermédiaire - paysage moins grossier
    Avant plan : paysage net

    Nous pouvons noter que le paysage dans le fond nuit un peu à la qualité graphique de l'ensemble. Cependant, l'ajout de brouillard permet aisément de gommer ce défaut en atténuant cette zone de l'image.
     

    --------------------------------- Retour à la liste des problèmes ---------------------------------

     

    Techniques d'optimisations générales
     
     
    1. Boucles

    De manière générale, nous devons sortir un maximum de calculs des boucles, et à plus forte raison encore, des boucles imbriquées. Ainsi, le calcul de la position d'un pixel dans le buffer ( position = BufferWidth * Y + X) pourra être calculée en dehors de la boucle puis incrémentée :

    Pour y allant de 0 à dimY
        Pour x allant de 0 à dimX
            Buffer[ dimX*y+x ] = ...
        Fin pour
    Fin pour

    devient :

    offset = 0
    Pour y allant de 0 à dimY
        Pour x allant de 0 à dimX
            Buffer[ offset ] = ...
            offset ++
        Fin pour
    Fin pour

    2. Fonction modulo

    La fonction modulo actuellement présente dans la classe Java n'est pas performante. En effet, nous pouvons nous limiter à une utilisation avec des textures et heightmaps de tailles correspondantes à des puissances de 2 (256. 512. 1024, ...). Ainsi, un modulo équivaudra à l'application d'un ET binaire :

    résultat = modulo( valeur, 256 ) sera équivalent à résultat = valeur & 255

    3. Tables précalculées

    Comme nous pouvons nous en rendre compte, nous allons avoir besoin d'accéder à des cos, sin et tan ; autant de calculs long à effectuer. Il peut donc sembler judicieux de précalculer des tables de valeur pour ces fonctions, selon la précision voulue. Ce sera le cas pour le calcul des vagues par exemple.

    --------------------------------- Retour au sommaire de cette page ---------------------------------



    Auteurs : Benjamin POCHAT et Vincent PRAT(site personnel)