Architecture d'un moteur de rendu

ou quelques idées pour en écrire un ...

les commentaires / remarques / compléments / correction d'erreurs, etc. sont les bienvenus : mailto:jean-claude.iehl@liris.cnrs.fr

hypothèses :

on suppose que le monde est trop gros pour être entièrement chargé en mémoire.
on suppose que le monde est découpé en cellules et que l'on peut localiser un point dans le monde (trouver la cellule dans laquelle il se trouve) et connaître les cellules voisines.

    dans le cas d'un monde ouvert (exterieurs) : un quadrillage régulier en 2D ou 2D5,
    dans le cas d'un monde fermé (intérieurs) : un système de cellules / portails, ou un bsp


on connait les caractéristiques du gpu :
    nombre de triangles affichables ("budget triangle")
    quantité de mémoire dédiée au stockage des triangles + données (attributs).
    quantité de mémoire dédiée aux textures ("buget texture").


étapes :

1. localiser le point de vue dans le monde : déterminer la cellule contenant le point et les cellules adjacentes
objectif : déterminer l'ensemble de cellules sur lequel on travaille

2. charger le contenu de l'ensemble de cellules

dans chaque cellule : ensemble d'objets (affichables ou pas)
dans chaque objet affichable : ensemble de "ressources 3D"
    géométrie
    animations (squelette, poids de skinning, etc.)
    textures
    matières
    effets / shaders
    + version procédurale (systèmes de particules, forêts, etc.)

3. estimation des objets visibles par la caméra ("frustum culling")
objectif : réduire la quantité de données à charger

4. sélection du niveau de détail géométrique
objectif : trouver le niveau de détail de chaque objet à afficher pour tenir le "budget triangle"

5. estimation exacte des objets visibles : "occlusion culling"
objectif : ne pas charger sur le gpu la géométrie des objets non visibles pour augmenter le niveau de détail des objets visibles en restant dans le "budget triangle". réevaluer le niveau de détail des objets visibles.

l'objectif des étapes 3, 4, 5 est de sélectionner le meilleur niveau de détail des objets à afficher qui respecte le "budget triangle".

6. charger la géométrie sur le gpu
compresser la géométrie :
    soit remailler l'objet à la volée (générer la géométrie sur le gpu) cf. tesselation,
    soit afficher une géometrie simple et ajouter des détails (lancer de rayons sur gpu), cf. relief mapping
compresser / quantifier les attributs de la géométrie pour limiter les données à charger lors de l'exécution des shaders

7. afficher la géométrie brute sans activer les shaders : "ZPass" avec un parcours de l'ensemble d'objets en s'éloignant de la caméra (utilisation efficace du Zbuffer)
objectif : remplir le Zbuffer pour accélérer l'étape 11.

8. estimer le niveau de détail (les niveaux de mip map utilisés) des textures / données des objets affichés à l'étape 7.
objectif : tenir le "budget texture"

9. rendu du niveau de détail des textures par le gpu.
objectif : ne pas charger sur le gpu les niveaux de mip map des textures qui ne seront pas utilisés pour l'affichage.
même idée que pour la géométrie et l'occlusion culling. permet de remonter le niveau de détail des autres textures.

l'objectif des étapes 8 et 9 est de tenir le "budget texture" en sélectionnant le meilleur niveau de détail pour chaque objet à afficher.

compression des textures.
compression / filtrge des normal maps

10. charger les textures sur le gpu

11. rendu final avec les shaders activés
objectif : je me demande bien ...

réordonner les shaders pour limiter le nombre de changements de l'état du pipeline,
l'étape 7 à déjà remplit le Zbuffer ce qui permet "d'oublier" d'afficher les objets en s'éloignant de la caméra (normalement pour utiliser efficacement le Zbuffer).
gestion des shaders : combien de sources, de quel type, combien de shadow map, quel effet (normal map, parallax map, relief map, ambient occlusion, etc.), quel post process (hdr, profondeur de champ, flou de mouvement, etc.)
limiter "l'explosion" du nombre de combinaisons de shaders "élémentaires" ...

12. limiter le nombre de batchs / appels de DrawPrimitive / glDrawElements

pourquoi ? parce que c'est la première cause de ralentissement du rendu gpu.
cf. "DirectX 9 Performance",
en résumé, il est de bon gout de ne pas dépasser 500 ~ 1000 batchs pour le rendu complet de la scène.

estimation rapide du nombre de batchs :
hypothèses :
    un objet est composé d'une ou plusieurs matières,
    une matière est composée d'une ou plusieurs textures et d'un ou de plusieurs shaders

    un objet est donc composé de plusieurs maillages associés chacun à une matière (texture + shader).


pour afficher une scène composée de plusieurs objets et éclairée par plusieurs sources de lumières :

afficher la scène éclairée par l'énergie ambiente
    pour chaque source
       pour chaque objet
          pour chaque maillage (associé à une matière)
             activer la matière ambiente
             afficher le maillage pour le rendu ambient
            
calculer les shadow maps
    pour chaque source
       pour chaque objet
          pour chaque maillage
             afficher le maillage

afficher la scène éclairée
    pour chaque source
       activer la shadow map
       pour chaque objet
          pour chaque maillage (associé à une matière)
             activer la matière complète
             afficher le maillage pour le rendu final

    Le nombre de batchs est dépendant du nombre de sources, du nombre d'objets visibles et du nombre de matières des objets. Ce qui offre plusieurs stratégies de réduction du nombre de batchs : travailler sur la représentation des objets et d'un groupe d'objets, travailler sur le nombre d'objets concerné par chaque étape, et travailler sur le nombre de sources de lumières.

    La simplification la plus directe consiste à utiliser le même shader pour calculer les aspects ambient et complet de chaque matière, ce qui permet d'éliminer la première étape ...

  1. utiliser l'instanciation pour afficher plusieurs objets avec un seul batch : ce n'est possible que lorsque certains objets sont affichés plusieurs fois (des arbres, de l'herbe, des unités dans un rts, etc.)

  2. éliminer le plus d'objets possible de chaque étape,
    1. cf. visibilité, frustum culling, occlusion culling,
    2. cf. gestion de ressources : par exemple, pour calculer les shadow maps, seule la forme des objets est nécessaire, il est possible de ré-organiser les maillages pour dessiner l'objet avec un seul batch
    3. construire l'ensemble d'objets visible par la caméra, l'ensemble d'objets visible pour chaque source, l'ensemble d'objets projettant une ombre sur les objets visibles par la caméra.

  3. limiter le nombre de maillages par objet,
    1. n'utiliser qu'une seule (mega-) texture qui couvre toute la scène,
      cf. "Unified Texture Management for Arbitrary Meshes" et id tech 5,
      "Sparse Virtual Textures" gdc 08

    2. n'utiliser qu'un seul shader capable de traiter toutes les matières / sources de lumière (restriction du type de matières ?),
    3. utiliser un atlas de textures (regouper plusieurs textures dans une seule)  et ré-organiser les  objets /  maillages  / coordonnées de textures.

  4. limiter le nombre de sources de lumières et de shadow map à calculer :
         utiliser une light map : précalculer les shadow maps des sources de lumières fixes

  5. accumuler les shadow maps de plusieurs sources dans la meme texture pour limiter le nombre de passes.

  6. ne dessiner qu'une seule fois la scène : "deferred rendering" et stocker toutes les informations nécessaires au rendu dans un / plusieurs buffers (position, normale, matière, etc.).

  7. utiliser du post process sur l'image de la scène : screen space ambient occlusion, depth of field, motion blur, rendu HDR, modifications colorimétriques,
    le temps de rendu ne dépend, généralement, que de la taille du frame buffer au lieu d'etre lié au nombre d'objets, et surtout ils sont déjà dessinés.

objets animés par vertex shader ?
    Render to Vertex Buffer, ati
    pixel_buffer_objects, extension opnGL nvidia ok, ati ?
    Stream Out, SM4, extension opengl nvidia sur geforce 8, directx 10


pour la suite :
    intégration du streaming ... cf. "Texture Streaming", insomniac 2007, "Advanced Virtual Texture Topics", crytek 2008
        gestion des ressources mémoire,

    gestion des ressources gpu (textures, vertex buffers, index buffers, etc.) :
        "Segment Buffering", "Optimizing Ressource Management with Multistreaming", gpu gems 2


    intégration avec le reste du moteur du jeu : gestion des entités ...


    interactions entre occlusion queries (augmente le nombre de batchs) pour éviter d'afficher les objets cachés et réduction du nombre de batchs ??
    autres questions :
        pour quel objet est-il rentable d'utiliser une occlusion query ? (les éléments du décors, ...)
        est-ce qu'un système logiciel peut etre compétitif ?



Références triées par étape :

les références sont organisées différement sur la page "programmation openGL"


étape 1, découpage du monde :

étape 2, charger le contenu des cellules :

étape 3, frustum culling :

étape 4, niveau de détail, LOD, générer des détails sur des objets peu détaillés :

étape 5, occlusion culling :

étape 6, charger la géométrie sur le gpu, vertex buffers et réordonnancement de sommets :

étape 7, Zpass :

étape 8, estimer les niveaux de mip map utilisés pour afficher une texture :

étape 9, rendu du niveau de mip map sur le gpu :

étape 10, charger les textures sur le gpu, gestion de textures :

étape 11, rendu final et effets :

étape 12, limiter le nombre de passes de rendu :