Structure du programme OpenGL et poo
J'ai travaillé sur une variété de projets de démonstration avec OpenGL et c++, mais ils ont tous impliqué simplement le rendu d'un seul cube (ou d'un maillage similaire) avec des effets intéressants. Pour une scène simple comme celle-ci, les données de sommet du cube peuvent être stockées dans un tableau global inélégant. Je cherche maintenant à rendre des scènes Plus Complexes, avec plusieurs objets de différents types.
Je pense qu'il est logique d'avoir différentes classes pour différents types d'objets (Rock
, Tree
, Character
, etc), mais je me demande comment décomposer proprement les données et les fonctionnalités de rendu pour les objets de la scène. Chaque classe stockera son propre tableau de positions de sommets, de coordonnées de texture, de normales, etc. Cependant, je ne sais pas où mettre les appels OpenGL. Je pense que j'aurai une boucle (dans une classe World
ou Scene
) qui parcourt tous les objets de la scène et les rend.
Devrait rendu impliquent l'appel d'une méthode de rendu dans chaque objet (Rock::render(), Tree::render(),...)
, ou une seule méthode render qui prend un objet en tant que paramètre (render(Rock), render(Tree),...)
? Ce dernier semble plus propre, car je n'aurai pas de code en double dans chaque classe (bien que cela puisse être atténué en héritant d'une seule classe RenderableObject
), et cela permet de remplacer facilement la méthode render() si je veux porter plus tard vers DirectX. D'un autre côté, Je ne suis pas sûr de pouvoir les garder séparés, car je pourrais avoir besoin de types spécifiques OpenGL stockés dans les objets de toute façon (tampons de vertex, par exemple). De plus, il semble un peu encombrant d'avoir la fonctionnalité de rendu est séparée de l'objet, car elle devra appeler beaucoup de méthodes Get()
pour obtenir les données des objets. Enfin, je ne suis pas sûr de savoir comment ce système gérerait les objets qui doivent être dessinés de différentes manières (différents shaders, différentes variables à transmettre aux shaders, etc.).
L'un de ces modèles est-il nettement meilleur que l'autre? De quelle manière puis-je les améliorer pour garder mon code bien organisé et efficace?
3 réponses
Tout d'abord, ne vous embêtez même pas avec l'indépendance de la plate-forme en ce moment. attendez d'avoir une bien meilleure idée de votre architecture.
Faire beaucoup d'appels De Tirage/changements d'état est lent. La façon dont vous le faites dans un moteur est que vous voudrez généralement avoir une classe rendue qui peut se dessiner. Ce rendu sera associé à tous les tampons dont il a besoin (par exemple, les tampons de vertex) et d'autres informations (comme le format de vertex, la topologie, les tampons d'index, etc.). Les mises en page d'entrée Shader peuvent être associé aux formats de vertex.
Vous voudrez avoir des classes GEO primitives, mais reporter tout ce qui est complexe à un type de classe mesh qui gère les tris indexés. Pour une application performante, vous souhaitez regrouper les appels (et potentiellement les données) pour des types d'entrée similaires dans votre pipeline d'ombrage afin de minimiser les changements d'état inutiles et les vidages de pipeline.
Les paramètres et les textures des Shaders sont généralement contrôlés via une classe de matériau associée à restituable.
Chaque rendu dans une scène elle-même est généralement un composant d'un nœud dans un graphe de scène hiérarchique, où chaque nœud hérite généralement de la transformation de ses ancêtres à travers un mécanisme. Vous voudrez probablement un récupérateur de scène qui utilise un schéma de partitionnement spatial pour déterminer rapidement la visibilité et éviter les frais généraux d'appel de tirage pour les choses hors de vue.
La partie scripting / comportement de la plupart des applications 3D interactives est étroitement liée ou accrochée à son graphique de scène cadre de noeud et un système d'événement/messagerie.
Tout cela s'intègre dans une boucle de haut niveau où vous mettez à jour chaque sous-système en fonction du temps et dessinez la scène à l'image actuelle.
Évidemment, il y a des tonnes de petits détails laissés de côté, mais cela peut devenir très complexe en fonction de la généralisation et de la performance que vous voulez être et du type de complexité visuelle que vous visez.
Votre question de draw(renderable)
, vs renderable.draw()
est plus ou moins hors de propos jusqu'à ce que vous déterminiez comment tous les les pièces s'emboîtent.
[mise à Jour] Après avoir travaillé dans cet espace un peu plus, certaines des connaissances:
Cela dit, dans les moteurs commerciaux, il ressemble généralement plus à draw(renderBatch)
où chaque lot de rendu est une agrégation d'objets homogènes d'une manière significative au GPU, car itérer sur des objets hétérogènes (dans un graphe de scène POO "pur" via polymorphisme) et appeler obj.draw()
un par un a une localité de cache horrible et est généralement une Ressources GPU. Il est très utile d'adopter une approche orientée données pour concevoir comment un moteur parle à ses API graphiques sous-jacentes de la manière la plus efficace possible, en répartissant les choses autant que possible sans affecter négativement la structure/lisibilité du code.
Une suggestion pratique est d'écrire un premier moteur en utilisant une approche naïve/"pure" pour se familiariser vraiment avec l'espace du domaine. Ensuite, sur un deuxième passage (ou probablement réécrire), concentrez-vous sur le matériel: des choses comme la mémoire représentation, localisation du cache, état du pipeline, bande passante, traitement par lots et parallélisme. Une fois que vous commencez vraiment à considérer ces choses, vous vous rendrez compte que la plupart de votre conception initiale sort par la fenêtre. Bon amusement.
Je pense que OpenSceneGraph est une sorte de réponse. Regardez-le et son implémentation . Il devrait vous fournir des idées intéressantes sur la façon d'utiliser OpenGL, C++ et OOP.
Voici ce que j'ai implémenté pour une simulation physique et ce qui a plutôt bien fonctionné et était à un bon niveau d'abstraction. D'abord, je séparerais la fonctionnalité en classes telles que:
- Objet conteneur qui contient toutes les informations de l'objet
- AssetManager-charge les modèles et les textures, les possède (unique_ptr), renvoie un pointeur brut vers les ressources de l'objet
- Renderer-gère tous les appels OpenGL, etc., alloue les tampons sur GPU et renvoie les handles de rendu des ressources à l'objet (lorsque le moteur de rendu souhaite dessiner l'objet, j'appelle le moteur de rendu en lui donnant le Model render handle, la texture handle et la Model matrix), renderer doit agréger ces informations pour pouvoir les dessiner par lots
- Physique-calculs qui utilisent l'objet avec ses ressources (sommets en particulier)
- scène-relie tout ce qui précède, peut également contenir un graphique de scène, dépend de la nature de l'application (peut avoir plusieurs graphes, BVH pour les collisions, autres représentations pour l'optimisation du tirage, etc.)
Le problème est que GPU est maintenant GPGPU (gpu à usage général) donc OpenGL ou Vulkan n'est plus seulement un framework de rendu. Par exemple, des calculs physiques sont effectués sur le GPU. Par conséquent, le renderer pourrait maintenant se transformer en quelque chose comme GPUManager et d'autres abstractions au-dessus. Aussi la façon la plus optimale de dessiner est dans un appel. En d'autres termes, un grand tampon pour toute la scène qui peut également être éditée via des shaders de calcul pour éviter une communication excessive CPU GPU.