portals
(niveau : avancé)


Table des matières

sections figures
Introduction sample "stencil depth"
Qu'est-ce qu'un portal ? plan appartement - scene graph
Comment ça marche ? réduction pyramide - culling
Quand est-ce utilisable ? utilisation dans Fairy
Le problème des cycles noeuds reliés par plusieurs portals
Objets partiellement visibles par des portals
Cas des objets à cheval sur des portals Goldorak changeant de pièce
Objets en mouvement
Autres propriétés miroir - portal off/on
Cellules concaves ou convexes ?
Conclusion
Référence
Pages apparentées
Annexe A : pseudo-code du parcours du scene graph
Annexe B : extrait du code de Fairy


Introduction

Les objets d'une scène 3D sont généralement stockés dans un scene graph, structure arborescente destinée à les hiérarchiser. La façon la plus simple d'afficher la scène telle qu'elle est vue depuis une caméra consiste à parcourir la totalité de l'arbre (= partir de sa racine, descendre récursivement dans chacune des branches, sous-branches, etc), et tracer tous les objets rencontrés qui sont totalement ou partiellement visibles. Un objet est considéré 'visible' lorsque son volume englobant (AABB, sphère ou autre) est au moins en partie contenu dans la pyramide de vision de la caméra.

Cette méthode (trop) simple possède deux inconvénients majeurs :

- le parcours de la totalité du graphe prend un temps non négligeable (d'autant qu'il ne peut pas entièrement tenir dans le cache). En fait l'algorithme n'a pas besoin d'explorer toutes les branches jusqu'au bout : si le volume englobant d'un noeud du graphe (= volume qui englobe tous les fils de ce noeud ainsi que les objets qu'il contient) est à l'extérieur de la pyramide de vision, il est inutile de descendre plus bas puisque tous les noeuds fils sont invisibles.

- l'overdraw peut être très important. Idéalement, chaque pixel de l'écran ne devrait être tracé qu'une seule fois par affichage de la scène ; l'overdraw désigne le fait qu'il peut en être autrement, que certains objets visibles se recouvrent, que certaines faces complètement masquées sont tracées pour rien. L'overdraw se mesure généralement en divisant le nombre de pixels affichés 'pour rien' (= nombre de pixels affichés moins nombre de pixels réellement visibles) par le nombre de pixels visibles ; sa valeur optimale est donc de 0, il est raisonnable jusqu'à 1 (= à chaque image un pixel est tracé 2 fois en moyenne), et devient problématique pour des valeurs supérieures. Sans descendre jusqu'au niveau de la face, une optimisation évidente consisterait à détecter les objets complètement cachés par d'autres, et à ne pas du tout les afficher ; ces tests d'occlusion sont malheureusement assez complexes (car les objets ont des formes compliquées, et ils ne peuvent pas être remplacés par leurs volumes englobants qui masquent forcément plus de choses qu'eux). C'est pourquoi il est préférable de faire l'inverse : détecter les zones qui permettent de voir 'ailleurs', par exemple dans la pièce d'à côté, grâce aux portals.


sample "stencil depth" de DirectX 8, montrant l'overdraw :
- en rouge les pixels tracés 1 fois
- en vert les pixels tracés 2 fois
- en jaune les pixels tracés 3 fois
- en bleu les pixels tracés 4 fois


Qu'est-ce qu'un portal ?

Il existe deux définitions différentes :

- pour certaines personnes (dont je suis), un portal est un polygone à n côtés, plan et convexe, traité de façon particulière au rendu. Son but n'est en effet pas d'être affiché, mais de délimiter une zone par laquelle 'on' peut passer d'un endroit du niveau à un autre (exemple : une porte entre deux pièces, une fenêtre, un trou dans un mur...). 'On' désigne notamment la vision : si une caméra située dans une pièce 'voit' un portal de cette pièce, cela signifie qu'elle peut aussi voir ce qui est 'de l'autre côté' du portal. Un portal établit donc un lien entre deux noeuds du graphe, qui n'ont pas forcément d'autre relation.

- d'autres personnes considèrent que ces polygones divisent la scène en sous-parties, et ce sont ces morceaux qu'ils appellent portals. Leurs 'portals' correspondent donc à mes 'pièces' ou 'salles', il ne s'agit plus de polygones mais de volumes, qui peuvent être convexes ou concaves, selon le but recherché (un volume concave peut toujours être subdivisé en plusieurs volumes tous convexes). Je préciserai cette notion de convexe / concave car elle est très importante, pour l'instant revenons à notre définition du portal : dans l'ensemble de ce document il désigne mon polygone plan et convexe à n côtés, mais sachez que dans d'autres textes il peut désigner un volume élémentaire de la scène (ou 'cellule').

Prenons l'exemple d'une maison : la racine du graphe représente la totalité de cette maison, chaque noeud situé sous la racine est une pièce, chaque pièce contient des meubles, dans les meubles il y a des objets, etc. C'est la même chose dans la démo #4 de Fairy : les noeuds de '1er niveau' (si la racine est au niveau 0) représentent chacun une pièce ou un couloir, leurs fils sont les objets placés dans cette pièce (par exemple pour la pièce centrale : 4 objets 'mur', 1 plafond, 1 sol, 1 escalier, 1 fontaine, 2 systèmes de particules, 1 lumière - le cas des portes est un peu à part car elles sont à la limite entre deux salles). Des portals sont placés sur chacun des 'trous' permettant de passer dans le couloir ou la pièce voisine, leurs formes correspondent exactement à celles de ces passages (de simples rectangles).

Note : puisque les portals sont des polygones, ils pourraient être stockés avec les faces 'normales', un simple flag indiquant qu'ils doivent être traités différemment ; il est cependant plus simple de les conserver dans une liste séparée, surtout qu'ils peuvent posséder des informations supplémentaires, comme nous le verrons par la suite.

exemple : plan approximatif de mon appartement (échelle non respectée)

A : chambre 1 - B : chambre 2 - C : salle de bain - D : couloir - E : WC - F : entrée - G : cuisine - H : séjour - I : balcon
rouge = porte - vert = fenêtre - rouge+vert = porte fenêtre

scene graph correspondant (une possibilité parmi d'autres), arrêté au niveau 1

en rouge : portals (numérotés en bleu) reliant les pièces


Comment ça marche ?

Le rendu de la scène ne commence plus à la racine du graphe, mais par le noeud qui contient la caméra concernée ; puisque la caméra est dans cette salle, celle-ci est forcément visible, et on commence par l'afficher ainsi que ses sous-noeuds (= ce qu'elle contient), en se limitant comme auparavant aux objets 'visibles' (= dans la pyramide de vision). Si parmi ces objets ou ceux des sous-noeuds il n'y a aucun portal visible, alors l'affichage est terminé ; si au contraire un portal est visible, le noeud auquel il mène est visité à son tour, et ainsi de suite récursivement.

Optimisation : lorsqu'un portal est visible, cela signifie que la pièce à laquelle il conduit est visible, mais seulement partiellement, puisque la caméra ne peut voir cette pièce qu'à travers le portal (et pas à travers les murs qui sont de chaque côté de la porte, par exemple). La pyramide de vision de la caméra peut donc être modifiée et réduite pour ne plus regarder que par l'embrasure de cette porte, ce qui a un double avantage : d'une part moins d'objets de la pièce voisine vont être détectés comme étant visibles et tracés pour rien (ce qui va réduire l'overdraw), mais surtout peu d'autres portals auront des chances d'être visibles, ce qui va très rapidement limiter le parcours du graphe. C'est là que réside le vrai avantage de la méthode.

réduction de la pyramide de vision en traversant les portals

en bleu : ce que voit la caméra - en gris : quelques objets 'invisibles' - en jaune : quelques objets 'visibles'

Entrons un peu dans les détails de la réduction de la pyramide de vision. Si un portal visible était toujours entièrement à l'intérieur de la pyramide de vision, il suffirait de remplacer cette dernière par le nouveau volume de vision plus petit, et le tour serait joué. Ce nouveau volume est défini de la façon suivante : chaque arête (= 2 points consécutifs) du polygone du portal forme avec la position de la caméra (= 3ème point) un plan, et l'ensemble des n plans délimite le volume. Ceci est vrai à condition que les 3 points servant à définir chaque plan soient distincts, ce qui implique que : - le polygone ne soit pas dégénéré, - la caméra ne soit pas exactement sur un des points du polygone, deux conditions faciles à satisfaire (dans un jeu la caméra possède un volume englobant, et ne peut pas s'approcher à moins d'une certaine distance des murs ou objets "collisionnables" (barbarisme courant dans le jeu vidéo), par exemple 50 cm).

Mais un portal peut n'être que partiellement visible (sur la figure ci-dessus : le portal 4 entre l'entrée et le couloir), ce qui veut dire que le nouveau volume 'dépasse' de la pyramide de vision, et n'est pas optimal (NB: un problème plus vicieux : la pyramide de vision est réduite pour les tests de visibilité, mais c'est toujours la pyramide d'origine qui est utilisée pour projeter les objets à l'écran, ce qui signifie que certains objets pourraient 'déborder' de l'écran, le clipping n'étant plus assuré correctement. En rendu software le résultat sera un plantage immédiat ; avec les cartes accélératrices le résultat est indéfini si vous utilisez un flag indiquant à la carte de ne pas clipper, parce que vous avez déterminé que l'objet traité est entièrement à l'intérieur du volume de vision). Le volume optimal est l'intersection de la pyramide initiale avec le nouveau volume, et il n'est même pas nécessaire de calculer cette intersection (ce qui simplifie l'algorithme).

Je suppose que vous savez déjà comment on détermine qu'un objet est dans la pyramide de vision d'une caméra : en fait on regarde s'il n'est pas en dehors de la pyramide de façon évidente, en testant s'il n'est pas entièrement 'à l'extérieur' par rapport à l'un des plans de la pyramide. Cette méthode n'est pas parfaite (certains objets vont être considérés comme visibles alors qu'ils ne le sont pas), mais a le mérite d'être simple, rapide, et tout de même très performante (elle élimine un grand pourcentage des objets invisibles, et conserve forcément tous les objets visibles). Elle peut être appliquée à n'importe quel volume convexe constitué de plans, et n'est pas réservée aux pyramides ;) Ceci explique pourquoi le portal doit être un polygone convexe : cette propriété garantit le fait que le volume qu'il va servir à créer avec la position de la caméra est lui aussi convexe.

détermination des objets qui sont dans la pyramide de vision (= "culling")

en bleu : la pyramide de vision
en gris : quelques objets 'invisibles' (à l'extérieur de la pyramide par rapport aux plans gauche 3 ou droit 4)
en jaune : un objet 'visible'
en orange : un objet supposé 'visible' (il n'est à l'extérieur de la pyramide ni par rapport au plan lointain 2, ni par rapport au plan de droite 4), à tort

L'intersection de 2 volumes convexes est un volume convexe (ou vide, ce qui signifierait que le portal est invisible), mais j'ai dit que nous n'allions pas la calculer : un objet est à l'intérieur de cette intersection s'il est à l'intérieur de chacun des 2 volumes. La pyramide de vision initiale est composée de 6 plans (haut/bas/gauche/droit/proche/lointain) ; lorsqu'un portal est traversé, les plans qu'il forme avec la caméra sont calculés, et ajoutés au volume de vision. Les objets traités ensuite sont testés par rapport à tous ces plans, jusqu'à ce que la routine récursive revienne dans le noeud de départ, et que les plans ajoutés par le portal soient dépilés. Dès qu'un objet est à l'extérieur du volume par rapport à l'un des plans, les tests s'arrêtent pour lui (il est invisible) ; il faut cependant éviter que le nombre de plans n'augmente trop rapidement sans quoi les tests deviennent plus longs, ce qui entraine la règle suivante : un portal doit rester une forme simple, ne contenant 'pas trop' d'arêtes. Si un trou dans un mur (par exemple) a une forme complexe, le portal qui le représente est simplement un polygone convexe qui englobe cette forme, dans la plupart des cas un rectangle (ou un losange, ou un parallélogramme) convient très bien.


Quand est-ce utilisable ?

J'ai beaucoup parlé de 'salles', 'pièces', portes, murs, etc. Les portals sont en effet particulièrement adaptés aux intérieurs, puisqu'ils évitent de tracer des objets ou des salles entières masqués par un mur. En extérieur la vision est beaucoup plus libre, moins limitée par des obstacles à courte distance, et des algorithmes d'occlusion (tests réels pour déterminer 'qui cache qui') sont plus indiqués.

La démo #4 de Fairy est un bon exemple de cas où l'utilisation des portals est très bénéfique : chaque salle contient des objets ayant beaucoup de polygones, mais on ne peut jamais voir plus de deux salles en même temps. Plus le monde est cloisonné, plus il y a d'objets (avec de nombreuses faces), plus c'est intéressant. Imaginez que j'agrandisse mon niveau de test, et que je multiplie le nombre de salles par 10, chacune contenant en moyenne autant de faces que les pièces actuelles ; si je m'arrange pour qu'on ne puisse toujours pas voir plus de 2 salles simultanément, les traitements resteront à peu près identiques et le framerate sera maintenu à sa valeur d'aujourd'hui.

utilisation des portals dans Fairy
salle principale cliquez pour agrandir
avec les portals cliquez pour agrandir
sans les portals cliquez pour agrandir
la différence est notable derrière Lara : sans les portals, la salle des chats est visible et affichée, alors qu'elle est entièrement cachée par trois murs (cliquez sur les images pour les agrandir).


Le problème des cycles

Le principe du rendu avec des portals n'a rien de compliqué, mais on voit tout de suite un problème apparaître : si un portal P relie deux nodes N0 et N1, et que lorsqu'on trace N0 le portal P est visible, on va alors parcourir N1, P va de nouveau être visible, et on va revenir dans N0 ! La fonction récursive de parcours du graphe va entrer dans une boucle infinie, et se terminer par un 'stack overflow' (dépassement de pile). Dans Fairy le problème ne se pose pas car les portals ne sont 'traversés' que s'ils font face à la caméra, c'est à dire si la caméra est du côté du plan du portal pointé par sa normale (comme tous les polygones, un portal possède une normale). Dans mon exemple, si la caméra est attachée au noeud N0 et qu'elle peut traverser le portal P (= il est dans la pyramide de vision, et sa normale pointe vers la caméra), lorsque le noeud N1 est affiché il est impossible de re-traverser P dans l'autre sens car pour le noeud N1 la normale est inversée. Pour être parfaitement exact, il est pénible de gérer une face ayant deux normales opposées, et il n'y a pas 1 mais 2 portals dans Fairy pour relier deux pièces entre elles : P0 qui permet de passer de N0 à N1, et P1 qui va dans le sens N1->N0. Les deux polygones sont identiques, mais ont des normales opposées (chacune pointe vers l'intérieur du noeud source de son portal). Ce système évite le cas d'un aller-retour simple entre deux noeuds, mais n'empêche pas des cycles plus complexes comme N0->N1->N2->N0, ou même N0->N1->N0 si l'aller et le retour ne se font pas par le même portal (deux pièces peuvent communiquer par plusieurs portes, ou fenêtres).

Les cycles n'apparaissent que si les pièces sont concaves, et ont des formes assez mal choisies, et il serait toujours possible de les éviter automatiquement ou à la main en découpant les pièces concaves en morceaux de pièce convexes. Il existe une autre solution : marquer les noeuds qui ont déjà été tracés, et ne pas les parcourir une deuxième fois. Cette solution ne fonctionne pas : si N0 et N1 communiquent par deux portes P0 et P1, depuis N0 on peut voir par P1 des objets de N1 qui ne sont pas visibles à travers P0, et qui n'ont donc pas été tracés lors du premier passage dans N1 (en supposant que l'algorithme traverse P0 avant P1). Ce sont donc en fait les objets qu'il faut marquer : qu'il soit vu à travers un portal ou un autre, un objet n'a qu'une seule position dans la scène, et sera tracé au même endroit de l'écran pour une caméra donnée.

deux noeuds N0 et N1 reliés par 3 portals

en bleu : la pyramide de vision
en orange : 2 objets 'visibles' dans N1
A est visible par le portal de gauche, B est visible par les deux autres portals (mais n'a besoin d'être tracé qu'une fois)

Pour éviter de devoir réinitialiser les flags 'objet déjà tracé' avant d'afficher chaque frame, on utilise un compteur (correspondant par exemple au numéro de la frame) sur 32 bits : lorsqu'un objet est visible, on compare la valeur de son compteur à la valeur courante, si elles sont différentes cela signifie que l'objet n'a pas encore été affiché pour cette frame, donc on l'affiche et on met à jour son compteur. Si les valeurs sont identiques, on passe à l'objet suivant.

Note : un objet peut être affiché plusieurs fois au cours de la même frame, s'il est en même temps vu et reflété par un miroir. Dans le cas d'un miroir plan (seul cas envisageable en temps réel), on ne crée pas le symétrique de l'objet par rapport au miroir, c'est la caméra qui est reflétée (c'est bien plus rapide, et le résultat est le même, cf vos cours d'optique). Pour autoriser un nouvel affichage de l'objet, il est possible de jouer avec le compteur de frames, et de l'incrémenter lorsque le miroir est 'traversé' (le miroir est un portal particulier : ses noeuds source et destination sont les mêmes, et il modifie la caméra lorsqu'elle le franchit).

Une autre façon de faire est de modifier la règle 'un objet ne peut être tracé qu'une seule fois par frame', qui est fausse avec les miroirs, par : 'un objet ne peut être affiché qu'une seule fois par frame depuis une caméra donnée'. Lorsqu'un miroir est détecté, une nouvelle caméra correspondant au reflet de la caméra courante est créée, et cette caméra peut de nouveau tracer tous les objets qui sont dans son champ de vision. Ceci implique de conserver dans l'objet caméra une liste des objets 3D déjà affichés, et de comparer chaque objet à cette liste avant de l'afficher (et de l'ajouter à la liste). Ces comparaisons prennent forcément un peu plus de temps que le système du compteur, mais c'est la méthode que j'utilise pour l'instant dans Fairy (ma liste est en fait un 'set' STL).

Si deux miroirs sont placés face à face, et que la direction de visée de la caméra est alignée avec leurs normales, elle va être reflétée sans arrêt par l'un puis l'autre miroir (dans une sorte de ping-pong infini). Dans ce cas, une solution consiste à introduire dans le portal un compteur indiquant combien de fois il a été traversé, et limiter cette valeur à 2 par exemple. Il faut alors penser à remettre ces compteurs à 0 au début de chaque frame (ce qui est bien plus rapide que dans le cas des objets, si on conserve dans la classe CPortal une liste statique de tous les portals en mémoire).


Objets partiellement visibles par des portals

Lorsqu'un objet n'est que partiellement visible par le polygone d'un portal, on pourrait être tenté de clipper le ou les morceaux qui dépassent. Cela n'est pas une bonne idée : le clipping est une opération un peu compliquée, qui génère des faces pouvant avoir plus de points que les faces d'origine (ex : le clipping d'un triangle donne un triangle ou un quad), les cartes TnL n'apprécient pas qu'on écrive dans leurs vertex buffers, et surtout c'est complètement inutile. Ces morceaux sont soit cachés par le mur (ou autre objet) qui entoure le portal, soit visibles par d'autres portals, et auront dans ce cas besoin d'être affichés. L'affichage 'pour rien' de certaines faces de l'objet n'est pas très coûteux (moins que de supprimer ces faces et d'en découper d'autres), si l'objet a un nombre de faces tel que ce problème puisse devenir préoccupant, c'est qu'il est mal modélisé : il faut le scinder en plusieurs sous-objets.

Il y a deux solutions pour que le mur (par exemple) cache l'objet correctement : afficher ce mur après l'objet (c'est à dire commencer par tracer ce qui est derrière le portal, et n'afficher qu'ensuite les objets de la pièce dans laquelle il est visible), ou laisser faire le z-buffer. Cette deuxième option est évidemment la plus intéressante, puisqu'elle permet de tracer les objets de chaque pièce dans n'importe quel ordre, et pas seulement de tracer les pièces dans n'importe quel ordre.

Le fait de ne pas clipper les objets par les plans des portals (il faut cependant toujours les clipper par ceux de la pyramide de vision de la caméra, mais les cartes TnL ou sinon les API s'en chargent) augmente un peu l'overdraw, dans des proportions très raisonnables. Si on veut absolument éviter cet overdraw supplémentaire, il y a toutefois moyen de le faire en utilisant des user clipping planes (plans de clipping utilisateur). C'est une solution que je déconseille en l'état actuel du matériel disponible : avec une carte non TnL c'est l'API qui va effectuer le clipping de façon inefficace, avec une carte TnL le nombre de plans de clipping personnalisés utilisables simultanément est très réduit (généralement 4 ou 6) et il ne faut pas oublier que la routine de parcours du graph est récursive, et empile en moyenne 4 plans par portal traversé, sur certaines consoles (Xbox) les user clipping planes n'existent pas réellement, et sont simulés par la carte grâce à une bidouille.

Certes les user clipping planes auraient l'avantage de ne plus avoir à effectuer aucun test de visibilité par rapport aux plans des portals, car en plus de clipper les objets partiellement visibles ils élimineraient totalement ceux qui ne le sont pas. Mais cette solution me paraît trop coûteuse car elle signifie que tous les objets sont envoyés à la carte 3D, même ceux qui peuvent être éliminés très rapidement avec quelques tests de leurs volumes englobants.


Cas des objets à cheval sur des portals

Goldorak en train de changer de pièce
(cf le message "CHR PORTAL COLL : TRUE" - cliquez pour agrandir l'image)
cliquez pour agrandir

Que se passe-t-il lorsqu'un objet est en même temps un peu des deux côtés d'un portal ? Imaginez par exemple une colonne dans un temple grec, tombée en travers d'une porte (je me suis amusé à faire ce test), ou plus probablement un objet mobile en train de franchir la porte pour passer d'une pièce à une autre (NB : les objets en mouvement et le franchissement des portals seront traités dans la section suivante, supposons pour l'instant que l'objet est fixe). Il doit être attaché aux deux pièces séparées par le portal, puisqu'il peut être visible dès que la caméra 'visite' une de ces deux pièces, même si le portal est en dehors de la pyramide de vision. Attacher un objet à deux noeuds du scene graph présente cependant plusieurs difficultés :

- si chaque salle possède son propre repère 3D local (position et orientation par rapport à son noeud parent), c'est à dire si tous les objets ne sont pas exprimés dans un seul et unique espace (celui de la scène), attacher un objet à deux noeuds revient à lui donner deux positions et deux orientations différentes. Bien qu'elles correspondent en définitive à un unique positionnement dans la scène, cela n'est généralement pas souhaitable. Par exemple, lorsque l'objet sera déplacé il faudra mettre ses deux positions à jour ; et que faudra-t-il faire lorsque l'un de ses noeuds parents (et pas l'autre) bougera : déplacer l'objet pour suivre un de ses parents, ou le laisser immobile avec l'autre ?

- si les noeuds auxquels l'objet est attaché sont tous les deux visibles, l'objet pourrait être tracé deux fois (à moins de le marquer comme expliqué dans la section sur les cycles).

- le problème ne se limite pas à deux noeuds : un objet peut très bien être à cheval sur davantage de 'cellules' du monde, elles ne représentent pas obligatoirement des salles comme dans Fairy et peuvent être très petites, surtout si les portals ne sont pas placés dans le niveau à la main mais par un algorithme de découpage automatique.

Une autre possibilité est envisageable : découper les objets à cheval sur des portals en plusieurs parties. Elle est peut-être séduisante dans le cas d'objets figés, mais plus du tout lorsqu'il s'agit d'objets animés ou qui se déplacent, et qu'il n'est plus question de précalculer les résultats.

J'ai donc choisi une autre solution. Chaque objet ne peut avoir qu'un seul parent (ou zéro s'il n'est pas attaché au graphe), ce qui simplifie les problèmes de positionnement (NB : cela n'empêche pas le partage de données = réutilisation d'un même objet à plusieurs endroits d'un niveau, simplement ce n'est pas l'objet (le mesh) qui est partagé, mais les données qu'il contient telles que ses faces et ses points). Un objet à cheval sur un ou des portals est attaché arbitrairement à l'un des noeuds concernés (par exemple celui qui contient la plus grosse portion d'objet. Dans Fairy, chaque objet étant défini dans son propre repère ou 'model space', il est attaché au noeud qui contient l'origine de ce repère). Afin d'enregistrer le fait qu'il est également présent dans d'autres parties de la scène, et peut être visible même si son parent ne l'est pas, il est attaché aux autres noeuds en tant que 'visiteur' (c'est un terme que j'ai inventé pour l'occasion). En plus de sa liste d'objets, de sous-noeuds, et de portals, chaque noeud a donc une liste de visiteurs. Ils sont traités comme les objets 'normaux' pour l'affichage et les collisions, mais tirent leur position / orientation de leur parent unique, sur lequel ils gardent un pointeur (comme tous les objets). Ils ne sont affichés qu'une fois par frame (sauf miroirs) grâce à la méthode expliquée plus haut (liste des objets affichés par une caméra donnée).

Un exemple de 'visiteurs' dans la démo #4 : les portes de la salle centrale, qui sont attachées aux couloirs mais aussi à cette salle. Et le personnage bien sûr, lorsqu'il passe d'un lieu à un autre.


Objets en mouvement

A un instant t, chaque objet en mouvement (personnage, caméra, etc) est attaché à un noeud, et se trouve dans une ou plusieurs cellules (= pièces) séparées par des portals. Qu'arrive-t-il lorsqu'un tel objet se déplace entre 2 frames ?

Le mouvement souhaité doit tout d'abord être validé : des tests de collision sont effectués avec les autres objets du noeud auquel l'objet est attaché (y compris les 'visiteurs'), avec les portals de ce noeud, puis avec les objets et portals des noeuds voisins si l'objet est effectivement à cheval sur un ou des portals, et ainsi de suite récursivement. S'il y a collision avec un objet, le mouvement est soit annulé, soit modifié (rebond sur un mur, etc) jusqu'à ce qu'il soit valide.

Lorsque le mouvement est accepté, si l'objet ne traverse aucun portal en effectuant son déplacement, il n'y a rien de particulier à faire. Au contraire si l'objet change de noeud (dans Fairy : si l'origine de l'objet passe de l'autre côté du portal), il faut à chaque portal traversé : détacher l'objet de son noeud courant, l'attacher au noeud auquel mène le portal, et calculer ses nouvelles position et orientation locales si chaque noeud possède son propre repère (c'est le cas dans Fairy).

Après le déplacement, si l'objet est en collision avec des portals, il ne faut pas oublier de l'attacher aux noeuds voisins concernés en tant que 'visiteur'. La scène peut ensuite être affichée, tout est prêt.

NB : je ne détaille pas ici la façon dont sont faits les tests de collision, ce sera le sujet d'un autre article. Il est très important qu'ils fonctionnent bien, si un objet traverse un portal sans s'en apercevoir (= sans changer de noeud parent) c'est toute la méthode qui tombe à l'eau.


Autres propriétés

Un portal peut avoir d'autres propriétés que le polygone et les noeuds source et destination qui le définissent. Par exemple, il peut contenir une transformation 3D (translation + rotation + scale...), qui modifie les caractéristiques de la caméra qui le traverse, comme sa position et son orientation. Deux utilisations classiques : les miroirs (la transformation est une symétrie par rapport au plan du portal, z = -z dans Fairy), et les téléporteurs ou caméras de surveillance (ce qui est affiché derrière le portal correspond à une autre partie de la scène). Ces effets sont obtenus quasiment gratuitement une fois que le parcours du scene graph via les portals est implémenté.

utilisation classique des portals : le miroir
cliquez pour agrandir
la géométrie sous le sol (pièces, Lara, fontaine, particules) n'existe pas réellement, c'est celle qui est vue par la caméra reflétée

Un portal peut également posséder un volume englobant son polygone si celui-ci a beaucoup de côtés, afin de déterminer plus rapidement s'il est dans la pyramide de vision de la caméra ; je rappelle toutefois qu'il est souhaitable d'avoir des polygones simples, afin de ne pas empiler trop de plans lors de la réduction du volume de vision.

Les portals de Fairy ont un flag on/off, qui indique pour chacun d'eux s'il est actif ou inactif. Un portal inactif ne sera pas traversé. Cela est utile lorsque le moteur ou le jeu sait que ce qui est derrière le portal est masqué et donc invisible. Ceci est démontré dans la démo #4 : lorsque les portes de la salle centrale sont complètement fermées, les portals correspondants sont désactivés, et les couloirs ou la salle centrale (selon le côté où l'on se trouve) ne sont pas tracés. Vous pouvez le voir en wireframe : ouvrez les portes, passez en wireframe (touche W), reculez pour laisser les portes se refermer, et observez ce qui se passe derrière elles.

portal désactivé / activé
portes fermées cliquez pour agrandir
portes qui s'ouvrent cliquez pour agrandir
(cliquez sur les images pour les agrandir)

Certains portals peuvent être traversés par des objets (ceux qui servent aux tests de visibilité, ou les téléporteurs), d'autres non (miroirs, caméras de surveillance). On peut ajouter un flag pour refléter cette caractéristique, mais c'est en général inutile : lorsqu'un portal est "bloquant", il y a souvent un mesh qui couvre le même emplacement, et qui suffit à empêcher les objets mobiles de passer.


Cellules concaves ou convexes ?

Il est temps de revenir sur un point important évoqué plus haut : les portals doivent-ils découper la scène en cellules concaves, ou convexes ? Si on opte pour les cellules concaves, toutes les formes sont permises sans restriction, ce qui simplifie la création de la scène ; si on choisit les cellules convexes, il faut découper celles qui sont concaves en plusieurs morceaux convexes, ce qui est toujours possible. Le choix se fait en fonction des avantages et inconvénients de chaque solution, et de l'importance que l'on accorde à chacun de ces avantages et inconvénients.

Avantages des cellules convexes :
- elles garantissent qu'il n'y aura pas du tout d'overdraw, ce qui était notre objectif de départ. Ceci signifie également que les objets ou morceaux d'objets visibles peuvent être tracés dans n'importe quel ordre, et sans l'aide du z-buffer, à condition de clipper ceux qui sont partiellement visibles (cf "Objets partiellement visibles par des portals" plus haut)
- les tests d'intersection et de collision sont plus simples avec des objets convexes qu'avec des objets concaves (= quelconques), avoir un monde constitué entièrement de cellules convexes est très pratique dans ce domaine
- de tout point d'un objet convexe on peut aller à un autre point de cet objet sans en sortir (c'est la définition même de la convexité) ; si un personnage doit se rendre d'un point A à un point B, il suffit de trouver entre ces deux points un chemin uniquement constitué de cellules vides, et le tour est joué : à l'intérieur de chacune de ces cellules le personnage pourra se déplacer sans rencontrer d'obstacle fixe (il peut toutefois heurter d'autres personnages ou objets mobiles). Ceci simplifie le pathfinding des PNJ (Personnages Non Joueurs, NPC en anglais).

Inconvénients des cellules convexes :
- le découpage de la scène en cellules convexes peut nécessiter un très grand nombre de portals (imaginez une sphère au centre d'une salle, et vous avez le cas le plus catastrophique puisque chaque face de la sphère va générer plusieurs portals). C'est peut-être le seul inconvénient mais il est très lourd, puisqu'il conduit aux points suivants :
- le découpage peut demander beaucoup de temps ; il s'agira donc d'une phase de pré-calcul (comme pour un BSP). Le monde sera figé, créer ou modifier des portals à la volée peut entrainer de nombreux traitements (afin de conserver la propriété de convexité)
- la scène occupera plus de place en mémoire, et sur le disque dur ou CD ROM
- les portals étant plus nombreux, et le graphe plus gros, le parcours de la scène prendra plus de temps
- les cellules étant en moyenne plus petites, les objets mobiles seront dans un plus grand nombre de cellules en même temps

Avantages des cellules concaves :
- les portals peuvent être placés à la main par le graphiste ou le designer, de façon plus intelligente qu'avec un découpage automatique
- les cellules reflètent l'architecture du niveau, elles correspondent à des entités logiques (pièces, couloirs, escaliers...)
- le parcours du graphe est rapide, moyennant un peu d'overdraw
- les objets partiellement visibles n'ont pas besoin d'être clippés (moyennant là aussi un peu d'overdraw)
- il est facile de modifier le niveau en temps réel, par exemple de créer un trou dans un mur (il suffit de savoir quel noeud se trouve de l'autre côté du mur)

Inconvénients des cellules concaves :
- l'overdraw n'est pas réduit à zéro
- les tests d'intersection et de collision sont plus complexes
- le découpage de la scène n'apporte aucune aide au niveau du pathfinding

A chacun de faire son choix. Dans Fairy les cellules sont concaves, car je ne suis pas partisan des longues phases de pré-calcul, et les mondes 3D devenant de plus en plus vastes et détaillés dans les jeux, il me semble mauvais (= trop long et coûteux en place mémoire) de les découper en infimes morceaux. Les cellules concaves permettent déjà de réduire fortement l'overdraw (et donc d'accélérer le rendu), sans compliquer énormément d'autres étapes du pipeline 3D.


Conclusion

On reproche généralement deux choses aux portals : ils génèrent beaucoup de tests (de visibilité), et de clipping (des objets partiellement visibles). Nous venons de voir que cela ne s'applique pas aux cas des cellules concaves, puisque le nombre de portals reste limité, et que les objets n'ont pas besoin d'être clippés.

Il n'est pas vraiment possible de comparer les portals au BSP (certains d'entre vous se posent sans doute cette question) : leurs buts ne sont pas les mêmes. Celui du BSP est d'avoir un ordre parfait d'affichage des objets d'avant en arrière ou d'arrière en avant quelle que soit la position de la caméra dans le monde (mais tout est affiché, l'overdraw est évité autrement), celui des portals avec des cellules convexes est de supprimer l'overdraw, celui des portals avec des cellules concaves est de limiter fortement et rapidement le nombre d'objets visibles moyennant quelques tests et données supplémentaires (peu nombreuses par rapport aux deux autres méthodes).


Référence

un tutorial très détaillé (en 17 parties !) sur Flipcode


Pages apparentées

Télécharger les articles
Télécharger les démos de Fairy


Annexe A : pseudo-code du parcours du scene graph

remarque : il s'agit en fait du code présenté dans l'annexe B, décrit et résumé en quelques mots (les 'détails' ont été omis)

parcours simple sans portal
noeud = racine du graph
noeud->ParcoursSimple(caméra)

noeud::ParcoursSimple(caméra)           // routine récursive
{
  noeud->Draw(caméra)                   // tracer le contenu du noeud (= les objets attachés)

  pour chaque fils du noeud             // = chaque sous-noeud
    fils->ParcoursSimple(caméra)

  pour chaque 'visiteur'                // = sous-noeud attaché à un autre noeud,
                                        //   mais qui dépasse (est visible) dans celui-ci
    visiteur->ParcoursSimple(caméra)
}

noeud::Draw(caméra)
{
  s'il y a au moins un objet dans le noeud : activer les lumières qui éclairent le noeud

  pour chaque objet du noeud
    si l'objet est un visiteur et qu'il a déjà été tracé : l'ignorer
    si l'objet est invisible : l'ignorer
    si le volume englobant de l'objet n'est pas dans la pyramide de vision : ignorer l'objet

    objet->Draw
    si l'objet est un visiteur : l'ajouter dans la caméra à la liste des objets déjà tracés
}
parcours avec des portals
noeud = salle dans laquelle se trouve la caméra
noeud->ParcoursPortals(caméra)

noeud::ParcoursPortals(caméra)
{
  réinitialiser la pyramide de vision   // = supprimer les plans ajoutés par les portals
  noeud->ParcoursPortalsRecursif(caméra)
}

noeud::ParcoursPortalsRecursif(caméra)
{
  pour chaque portal attaché au noeud
    dest = portal destination correspondant
    noeuddest = noeud du portal destination

    si le portal est désactivé, l'ignorer
    si le volume englobant du portal n'est pas dans la pyramide de vision, ignorer le portal
    si le volume englobant du portal n'est pas dans la pyramide réduite, ignorer le portal

    transformer la position de la caméra dans le repère du portal
    si la caméra n'est pas face au portal (= du côté pointé par sa normale), ignorer le portal

    transformer le volume englobant du portal (une AABB) dans le repère de la caméra
    calculer les équations des plans formés par la caméra et les arêtes du volume transformé
    ajouter ces plans à la pyramide de vision de la caméra

    si le portal possède une transformation 3D (miroir, téléporteur...)
      calculer  position et orientation de la caméra après transformation
      créer une nouvelle caméra avec ces paramètres
      copier les plans de la caméra courante dans la nouvelle caméra

      noeuddest->ParcoursPortalsRecursif(nouvelle caméra)

    sinon
      noeuddest->ParcoursPortalsRecursif(caméra)

    supprimer les plans ajoutés à la caméra par ce portal

  //

  noeud->Draw(caméra)                   // routine décrite plus haut

  //

  pour chaque fils du noeud             // = chaque sous-noeud
    fils->ParcoursPortalsRecursif(caméra)

  pour chaque 'visiteur'                // = sous-noeud attaché à un autre noeud,
                                        //   mais qui dépasse (est visible) dans celui-ci
    visiteur->ParcoursPortalsRecursif(caméra)
}

Annexe B : extrait du code de Fairy

Je suis sûr que certains d'entre vous veulent voir du 'vrai' code, même s'il ne s'agit que de quelques fonctions, fournies sans explications supplémentaires. Les deux premières correspondent au parcours 'simple', les deux suivantes au parcours utilisant les portals.

Si vous regardez d'un peu plus près vous verrez que j'utilise la STL pour les listes de noeuds / portals / visiteurs. Ce code n'est qu'un exemple sujet à de futures modifications, certaines parties comme l'écriture dans le stencil buffer ne sont pour le moment plus utilisées (NB : le stencil est un autre moyen de clipper les objets partiellement visibles par un portal).

Si vous décidez un jour d'écrire votre propre système de portals, je vous engage à ne pas en tenir compte, et à vous baser uniquement sur l'article ci-dessus, ou toute autre source d'information sur le sujet. Les algorithmes sont importants, pas l'implémentation particulière de telle ou telle personne. A vos claviers ;)

/**********************************************************************************************************************/
/*                                                GRAPH TRAVERSAL                                                     */
/**********************************************************************************************************************/

/************************************************ dwSimpleTraversal ***************************************************/
// traverse graph to render visible objects - init                                                                      
// 22/04/01, M                                                                                                          
// in : current camera,current frame number                                                                             
// out: number of nodes traversed                                                                                       
// rem: simply follow the tree, no portals                                                                              
/**********************************************************************************************************************/

DWORD CGraphNode::dwSimpleTraversal(const CCamera* lpCamera,const DWORD dwFrame)
  {
  DWORD dwNodes = 0;
  vSimpleTraversal(lpCamera,dwFrame,&dwNodes);
  return dwNodes;
  }

/************************************************ vSimpleTraversal ****************************************************/
// traverse node & children (recursively)                                                                               
// 22/04/01, M                                                                                                          
// in : current camera,current frame number,node counter address                                                        
// out:                                                                                                                 
// rem: simply follow the tree, no portals                                                                              
/**********************************************************************************************************************/

void CGraphNode::vSimpleTraversal(const CCamera* lpCamera,const DWORD dwFrame,DWORD* lpdwNodes)
  {
  CCamera* lpCam   = const_cast<CCamera*>(lpCamera);
//  if(m_dwTraversalSee == dwFrame) return;                   // already traversed; avoid infinite loop
  m_dwTraversalSee = dwFrame;
  (*lpdwNodes)++;

  // traverse node

  hrDraw(lpCamera);

  // traverse children

  listNodeIterator itChild = m_listChildren.begin();        // DON'T use lpGetFirstChild because of shared iterator !

  while(itChild != m_listChildren.end())
    {
    CGraphNode* lpChild  = *itChild;
    CBVbase*    lpBVNode = lpChild->lpGetBoundingVol();
    CMat4x4     m4Node2World;

    if(lpBVNode)
      {
      if(lpChild->boGetWorldMatrix(&m4Node2World))
        lpBVNode->vSetTrf2World(&m4Node2World);
      }

    lpChild->vSimpleTraversal(lpCamera,dwFrame,lpdwNodes);
    itChild++;
    }

  // traverse visitors

  setNodeIterator itVisitor = m_setVisitors.begin();

  while(itVisitor != m_setVisitors.end())
    {
    CGraphNode* lpVisitor = *itVisitor;
    CBVbase*    lpBVNode  = lpVisitor->lpGetBoundingVol();
    CMat4x4     m4Node2World;

    if(lpBVNode)
      {
      if(lpVisitor->boGetWorldMatrix(&m4Node2World))
        lpBVNode->vSetTrf2World(&m4Node2World);
      }

    lpVisitor->vSimpleTraversal(lpCamera,dwFrame,lpdwNodes);
    itVisitor++;
    }
  }

/************************************************ dwTraversal *********************************************************/
// traverse graph to render visible objects - init                                                                      
// 27/04/00, M                                                                                                          
// in : current camera,current frame number                                                                             
// out: number of nodes traversed                                                                                       
/**********************************************************************************************************************/

DWORD CGraphNode::dwTraversal(const CCamera* lpCamera,const DWORD dwFrame)
  {
  lpCamera->lpGetBVPlanes()->vSetTrf2World(lpCamera->lpm4Get2WorldMatrix());
  lpCamera->lpGetBVPlanes()->vRemoveAll();                  // planes are in camera space

  DWORD dwNodes = 0;
  vTraversal(lpCamera,dwFrame,&dwNodes);
  return dwNodes;
  }

/************************************************ vTraversal **********************************************************/
// traverse node & children (recursively)                                                                               
// 27/04/00, M                                                                                                          
// in : current camera,current frame number,node counter address                                                        
// out:                                                                                                                 
/**********************************************************************************************************************/

void CGraphNode::vTraversal(const CCamera* lpCamera,const DWORD dwFrame,DWORD* lpdwNodes)
  {
  CCamera* lpCam   = const_cast<CCamera*>(lpCamera);
//  if(m_dwTraversalSee == dwFrame) return;                   // already traversed; avoid infinite loop
  m_dwTraversalSee = dwFrame;
  (*lpdwNodes)++;

  // traverse portals

  listPortalIterator itPortal = m_listPortals.begin();        // DON'T use lpGetFirstPortal because of shared iterator !

  while(itPortal != m_listPortals.end())
    {
    CGraphPortal* lpPortal   = *itPortal;
    CGraphPortal* lpGoal     = lpPortal->lpGetGoal();
    if(!lpGoal)
      {
      if(lpPortal->dwGetType() == CGraphPortal::_MIRROR_)
        lpGoal = lpPortal;
      else
        {
        itPortal++;
        continue;
        }
      }

    CGraphNode  * lpNode     = lpGoal  ->lpGetParentNode();
    CBVbase     * lpBVPortal = lpPortal->lpGetBoundingVol();

    // set portal->world trf

    CMat4x4 m4Portal2World;
    if(lpBVPortal && lpPortal->boGetWorldMatrix(&m4Portal2World))
      lpBVPortal->vSetTrf2World(&m4Portal2World);

    // transform camera position to portal repere

    CVect3D  v3CamWorldPos;
    CVect3D  v3CamPortalPos;

    // portal in camera frustum ?

    if(lpPortal->boIsEnabled() && lpCam->dwIsInFrustum(lpPortal))
      {
      // test against previous portals
      // rem: planes are in camera space (to be node-independent)

      CBVPlanes* lpPlanes = lpCam->lpGetBVPlanes();

      if(lpBVPortal && (lpPlanes->hrIntersection(lpBVPortal) != CBVbase::_OUTSIDE_))
        {
        if(lpCam->boGetNodeWorldPos(&v3CamWorldPos) && lpPortal->boTransformToRepere(&v3CamPortalPos,v3CamWorldPos))
          {
          if(v3CamPortalPos[_Z_] > 0.f)
            {                                               // portal facing the camera
            // test : draw portal (mesh) to stencil

            if(m_lpRenderer->boAllowStencil())
              {
              m_lpRenderer->vSetStencilWrite();
              hrDraw(lpCamera);
              m_lpRenderer->vSetStencilTest();
              }

            // push planes

            DWORD dwPlaneNum[4];

            if(lpBVPortal && m_boPortalCulling)
              {
              CBVAABB* lpPortalAABB = reinterpret_cast<CBVAABB*>(lpBVPortal);
              CVect3D  v3Min,v3Max;
              lpPortalAABB->vGet(&v3Min,&v3Max);

              CVect3D  v3Corner[4];                         // build corners (portal seen front => X to the left)
              v3Corner[0].vSet(v3Max[_X_],v3Min[_Y_],0.f);
              v3Corner[1].vSet(v3Max[_X_],v3Max[_Y_],0.f);
              v3Corner[2].vSet(v3Min[_X_],v3Max[_Y_],0.f);
              v3Corner[3].vSet(v3Min[_X_],v3Min[_Y_],0.f);

//              CMat4x4  m4World2Cam;
//              lpCam->boGetNodeWorldMatrixI(&m4World2Cam);
//              CMat4x4  m4Portal2Cam = m4World2Cam*m4Portal2World;
              CMat4x4  m4Portal2Cam = (*lpCam->lpm4Get2CamMatrix()) * m4Portal2World;

              CVect3D  v3Proj[5];                           // trf to camera space
              v3Proj[0].vSet(m4Portal2Cam*v3Corner[0],_W_);
              v3Proj[1].vSet(m4Portal2Cam*v3Corner[1],_W_);
              v3Proj[2].vSet(m4Portal2Cam*v3Corner[2],_W_);
              v3Proj[3].vSet(m4Portal2Cam*v3Corner[3],_W_);
              v3Proj[4].vSet(v3Proj[0]);

              // build plane eq

              for(DWORD dwPlane = 0; dwPlane < 4; dwPlane++)
                {
                CVect3D v3_01(v3Proj[dwPlane+1]-v3Proj[dwPlane]);
                CVect3D v3N  (v3Proj[dwPlane]  ^v3_01);
                if(lpCam->boIsLeftHanded()) v3N = -v3N;
                CVect4D v4Eq (v3N,0.f);

                dwPlaneNum[dwPlane] = lpPlanes->dwAddPlane(v4Eq);
                }
              }

            // portal changes camera parameters

            if(lpPortal->boUseSpecial())
              {
              // trf to portal space

              CMat4x4 m4I;
              lpPortal->boGetWorldMatrixI(&m4I);
              m4I[_X_][_W_] = m4I[_Y_][_W_] = m4I[_Z_][_W_] = 0.f;

              CVect3D v3CamWorldDir,v3CamWorldUp;
              lpCam->boGetNodeWorldDir(&v3CamWorldDir);
              lpCam->boGetNodeWorldUp (&v3CamWorldUp);

              CVect3D v3CamPortalDir(m4I*v3CamWorldDir,_W_);
              CVect3D v3CamPortalUp (m4I*v3CamWorldUp ,_W_);

              // apply special trf (no translation)

              CMat4x4* lpm4Trf = lpPortal->lpm4GetSpecialTrf();
              v3CamPortalDir.vSet((*lpm4Trf)*v3CamPortalDir,_W_);
              v3CamPortalUp .vSet((*lpm4Trf)*v3CamPortalUp ,_W_);
              v3CamPortalPos.vSet((*lpm4Trf)*v3CamPortalPos,_W_);

              // back to world space

              CMat4x4 m4M;
              lpPortal->boGetWorldMatrix(&m4M);
              m4M[_X_][_W_] = m4M[_Y_][_W_] = m4M[_Z_][_W_] = 0.f;

              v3CamWorldDir.vSet(m4M*v3CamPortalDir,_W_);
              v3CamWorldUp .vSet(m4M*v3CamPortalUp ,_W_);
              lpPortal->boTransformToWorld(&v3CamWorldPos,v3CamPortalPos);

              // new camera

              CGraphNode nodNew;
              CCamera    camNew(*lpCam);
              nodNew.vSetAutoDelete(false);                 // local vars can't delete themselves
              camNew.vSetAutoDelete(false);

              nodNew.boAttachObject (&camNew);
              nodNew.vSetPosition   (v3CamWorldPos);
              nodNew.vSetOrientation(v3CamWorldDir,v3CamWorldUp);

              camNew.vSetLeftHanded(!lpCam->boIsLeftHanded());
              camNew.boCalcTrfMatrix();

              // copy BV planes

              DWORD      dwPlanes    = lpPlanes->dwGetNbPlanes();
              CBVPlanes* lpNewPlanes = camNew.lpGetBVPlanes();
//              CMat4x4    m4Cam2World;
//              camNew.boGetNodeWorldMatrix(&m4Cam2World);
//              lpNewPlanes->vSetTrf2World (&m4Cam2World);
              lpNewPlanes->vSetTrf2World(camNew.lpm4Get2WorldMatrix());

              for(DWORD dwPlane = 0; dwPlane < dwPlanes; dwPlane++)
                {
                lpNewPlanes->dwAddPlane(lpPlanes->v4GetPlane(dwPlane,NULL));
                }

              // recurse

              m_lpRenderer->boBegin3D(&camNew);             // new cam->world trf

              m_lpRenderer->vSetCullMode(camNew.boIsLeftHanded());
              lpNode->vTraversal(&camNew,dwFrame,lpdwNodes);
              m_lpRenderer->vSetCullMode(!camNew.boIsLeftHanded());

              m_lpRenderer->boBegin3D(lpCam);               // restore
              nodNew.boDetachObject(&camNew);               // don't forget that, the camera could be destroyed before the node
              }

            // same camera

            else
              {
              lpNode->vTraversal(lpCamera,dwFrame,lpdwNodes);
              }

            // pop planes

            if(lpBVPortal && m_boPortalCulling)
              {
              lpPlanes->boRemovePlane(dwPlaneNum[0]);
              lpPlanes->boRemovePlane(dwPlaneNum[1]);
              lpPlanes->boRemovePlane(dwPlaneNum[2]);
              lpPlanes->boRemovePlane(dwPlaneNum[3]);
              }

            // disable stencil

            if(m_lpRenderer->boAllowStencil())
              {
              m_lpRenderer->vSetStencilOff();
              }
            }
          }
        }
      }

    itPortal++;
    }

  // traverse node

  hrDraw(lpCamera);

  // traverse children

  listNodeIterator itChild = m_listChildren.begin();        // DON'T use lpGetFirstChild because of shared iterator !

  while(itChild != m_listChildren.end())
    {
    CGraphNode* lpChild  = *itChild;
    CBVPlanes*  lpPlanes = lpCam  ->lpGetBVPlanes();
    CBVbase*    lpBVNode = lpChild->lpGetBoundingVol();
    CMat4x4     m4Node2World;

    if(lpBVNode)
      {
      if(lpChild->boGetWorldMatrix(&m4Node2World))
        lpBVNode->vSetTrf2World(&m4Node2World);
      }

//!!!    if(!lpBVNode || (lpPlanes->hrIntersection(lpBVNode) != CBVbase::_OUTSIDE_))
      {
      lpChild->vTraversal(lpCamera,dwFrame,lpdwNodes);
      }

    itChild++;
    }

  // traverse visitors

  setNodeIterator itVisitor = m_setVisitors.begin();

  while(itVisitor != m_setVisitors.end())
    {
    CGraphNode* lpVisitor = *itVisitor;
    CBVbase*    lpBVNode  = lpVisitor->lpGetBoundingVol();
    CMat4x4     m4Node2World;

    if(lpBVNode)
      {
      if(lpVisitor->boGetWorldMatrix(&m4Node2World))
        lpBVNode->vSetTrf2World(&m4Node2World);
      }

    lpVisitor->vTraversal(lpCamera,dwFrame,lpdwNodes);
    itVisitor++;
    }
  }

/**********************************************************************************************************************/
/*                                                OVERRIDABLES                                                        */
/**********************************************************************************************************************/

/************************************************ hrDraw **************************************************************/
// draw all objects in node                                                                                             
// 28/04/00, M - 05/10/00 visibility test                                                                               
// in : current camera                                                                                                  
// out: _OK_ or error ID                                                                                                
/**********************************************************************************************************************/

HRESULT CGraphNode::hrDraw(const CCamera* lpCamera)
  {
  if(!lpCamera) return _ERR_(_GLOBERR_BADPARAM_);
  CCamera*   lpCam    = const_cast<CCamera*>(lpCamera);
  CBVPlanes* lpPlanes = lpCam->lpGetBVPlanes();

  // objects

  listObjIterator itObj = m_listObjects.begin();
  if(itObj != m_listObjects.end())
    {
    vUpdateBaseLights();                                    // update node's list of lights
    m_lpRenderer->boUpdateLights(this);                     // enable/disable renderer lights
    }

  while(itObj != m_listObjects.end())
    {
    CGraphObj* lpObj  = *itObj;
    bool       boDraw = true;
    bool       boAddV = false;

    if(lpObj->boIsVisitor())
      {
      if(lpCam->boAlreadyDrawn(lpObj)) boDraw = false;
      else                             boAddV = true;
      }

    boDraw &= lpObj->boIsVisible();

    if(boDraw)
      {
      CMat4x4  m4Obj2World;
      CBVbase* lpBV = lpObj->lpGetBoundingVol();
      if(lpBV && boGetWorldMatrix(&m4Obj2World))
        lpBV->vSetTrf2World(&m4Obj2World);

      if(lpCam->dwIsInFrustum(lpObj))
        {
        lpObj->vSetCurrentCamera(lpCamera);
        if(!lpBV || (lpPlanes->hrIntersection(lpBV) != CBVbase::_OUTSIDE_))
          {
          lpObj->hrDraw();
          if(boAddV) lpCam->boAddVisitor(lpObj);            // only when it's really drawn !
          }
        if(lpBV) lpBV->hrDraw(this);
        }
      }

    itObj++;
    }

  // node's BV

  CBVbase* lpBV = lpGetBoundingVol();
  if(lpBV) lpBV->hrDraw(this);
  return _ERR_(_OK_);
  }

haut de la page