Rouler mon propre graphique de scène

23

Bonjour Game Development SE!

Je me fraye un chemin à travers OpenGL dans l'espoir de créer un moteur de jeu simple et très léger. Je considère le projet comme une expérience d'apprentissage qui pourrait faire un peu d'argent à la fin, mais qui sera amusante de toute façon.

Jusqu'à présent, j'ai utilisé GLFW pour gagner des E / S de base, une fenêtre (avec une touche plein écran F11 tellement sophistiquée) et bien sûr un contexte OpenGL. J'ai également utilisé GLEW pour exposer le reste des extensions OpenGL car j'utilise Windows et je veux utiliser tout OpenGL 3.0+.

Ce qui m'amène au graphique de la scène. Bref, j'aimerais rouler le mien. Cette décision est venue après avoir regardé OSG et lu quelques articles sur la façon dont le concept d'un graphique de scène est devenu tordu, courbé et cassé. Un de ces articles décrit comment les graphiques de scène se sont développés comme ...

Ensuite, nous avons ajouté toutes ces choses supplémentaires, comme accrocher des ornements sur un arbre de Noël, sauf que certains des ornements sont de jolis steaks juteux et d'autres sont des vaches vivantes entières.

Après l'analogie, j'aimerais le steak, la viande de ce que devrait être un graphique de scène, sans avoir à attacher des tas de code supplémentaire ou des vaches entières.

Donc, avec cela à l'esprit, je me demande exactement ce qu'un graphique de scène devrait être et comment un graphique de scène simple devrait être mis en œuvre? Voici ce que j'ai jusqu'à présent ...

Un arbre à un parent, n-enfants ou DAG qui ...

  • Devrait garder une trace des transformations des objets de jeu (position, rotation, échelle)
  • Doit contenir des états de rendu pour les optimisations
  • Doit fournir un moyen d'éliminer les objets qui ne sont pas dans le tronc de la vue

Avec les propriétés suivantes ...

  • Tous les nœuds doivent être traités comme pouvant être rendus (même s'ils ne sont pas rendus) Cela signifie qu'ils ...

    • Devraient tous avoir les méthodes cull (), state () et draw () (retourner 0 si non visible)
    • cull () appelle récursivement cull () sur tous les enfants, générant ainsi un maillage de cull complet pour le nœud entier et tous les enfants. Une autre méthode, hasChanged () pourrait permettre aux maillages dits statiques de ne pas avoir besoin que leur géométrie d'élimination soit calculée à chaque image. Cela fonctionnerait de telle sorte que si un nœud de la sous-arborescence a changé, toute la géométrie jusqu'à la racine est reconstruite.
  • Les états de rendu seront conservés dans une énumération simple, chaque nœud sélectionnera dans cette énumération un ensemble d'états OpenGL dont il a besoin et cet état sera configuré avant l'appel de draw () sur ce nœud. Cela permet le traitement par lots, tous les nœuds d'un jeu d'états donné seront rendus ensemble, puis le jeu d'états suivant est configuré, etc.

  • Aucun nœud ne doit contenir directement les données de géométrie / shader / texture, mais les nœuds doivent plutôt pointer vers des objets partagés (peut-être gérés par un objet singleton comme un gestionnaire de ressources).

  • Les graphiques de scène doivent pouvoir référencer d'autres graphiques de scène (peut-être en utilisant un nœud proxy) pour permettre des situations comme celle-ci , permettant ainsi de copier des modèles / objets complexes à plusieurs mailles autour du graphique de scène sans ajouter une tonne de données.

J'espère obtenir de précieux commentaires sur ma conception actuelle. Manque-t-il des fonctionnalités? Existe-t-il un modèle / modèle de conception bien meilleur? Me manque-t-il un concept plus large qui devra être inclus dans cette conception pour un jeu 3D quelque peu simple? Etc.

Merci, -Cody

Cody Smith
la source

Réponses:

15

Le concept

Fondamentalement, un graphe de scène n'est rien de plus qu'un graphe acyclique bidirectionnel qui sert à représenter un ensemble hiérarchiquement structuré de relations spatiales.

Les moteurs à l'état sauvage ont tendance à inclure d'autres goodies dans le graphique de la scène, comme indiqué. Que vous voyiez cela comme la viande ou la vache dépend probablement de votre expérience avec les moteurs et les bibliothèques.

Le garder léger

Je préfère le style Unity3D d'avoir votre nœud de graphe de scène (qui en son cœur est une structure topologique plutôt qu'une structure spatiale / topographique) inclut intrinsèquement des paramètres spatiaux et des fonctionnalités. Dans mon moteur, mes nœuds sont encore plus légers que Unity3D, où ils héritent de nombreux membres indésirables inutiles des superclasses / interfaces implémentées: voici ce que j'ai - à peu près aussi léger que possible:

  • membres du pointeur parent / enfant.
  • membres de paramètres spatiaux pré-transformés: position xyz, tangage, lacet et roulis.
  • une matrice de transformation; les matrices d'une chaîne hiérarchique peuvent se multiplier très rapidement et facilement en marchant récursivement vers le haut / bas dans l'arbre, vous donnant les transformations spatiales hiérarchiques qui sont la caractéristique principale d'un graphique de scène;
  • une updateLocal()méthode qui met à jour uniquement les matrices de transformation de ce nœud
  • une updateAll()méthode qui met à jour cela et toutes les matrices de transformation des nœuds descendants

... J'inclus également des logiques d'équations de mouvement et donc des éléments de vitesse / accélération (linéaires et angulaires) dans ma classe de nœuds. Vous pouvez y renoncer et le gérer dans votre contrôleur principal à la place si vous le souhaitez. Mais c'est tout - très léger en effet. N'oubliez pas que vous pourriez les avoir sur des milliers d'entités. Donc, comme vous l'avez suggéré, gardez-le léger.

Construire des hiérarchies

Que dites-vous d'un graphe de scène référençant d'autres graphes de scène ... J'attends la punchline? Bien sûr qu'ils le font. C'est leur principale utilisation. Vous pouvez ajouter n'importe quel nœud à n'importe quel autre nœud et les transformations devraient se produire automatiquement dans l'espace local de la nouvelle transformation. Tout ce que vous faites, c'est changer un pointeur, ce n'est pas comme si vous copiez des données! En changeant un pointeur, vous avez alors un graphique de scène plus profond. Si l'utilisation de procurations rend les choses plus efficaces que jamais, mais je n'en ai jamais vu le besoin.

Évitez la logique liée au rendu

Oubliez le rendu lorsque vous écrivez votre classe de nœuds de graphe de scène, ou vous confondrez les choses par vous-même. Tout ce qui compte, c'est que vous ayez un modèle de données - que ce soit le graphe de la scène ou non - et que certains moteurs de rendu vont inspecter ce modèle de données et rendre les objets dans le monde en conséquence, que ce soit en 1, 2 , 3 ou 7 dimensions. Le point que je fais est le suivant: ne contaminez pas votre graphique de scène avec une logique de rendu. Un graphique de scène concerne la topologie et la topographie - c'est-à-dire la connectivité et les caractéristiques spatiales. Ce sont le véritable état de la simulation et existent même en l'absence de rendu (qui peut prendre n'importe quelle forme sous le soleil d'une vue à la première personne à un graphique statistique à une description textuelle). Les nœuds ne pointent pas vers des objets liés au rendu, mais l'inverse peut bien être vrai. Considérez également ceci: Tous les nœuds de graphe de scène de votre arborescence ne seront pas tous rendus. Beaucoup ne seront que des conteneurs. Alors pourquoi même allouer de la mémoire à un objet pointeur vers rendu? Même un membre de pointeur qui n'est jamais utilisé prend encore de la mémoire. Inversez donc la direction du pointeur: l'instance liée au rendu fait référence au modèle de données (qui peut être ou inclure votre nœud de graphe de scène), PAS vice versa. Et si vous voulez un moyen simple de parcourir votre liste de contrôleurs tout en ayant accès à la vue associée, utilisez un dictionnaire / table de hachage, qui approche le temps d'accès en lecture O (1). De cette façon, il n'y a pas de contamination, et votre logique de simulation ne se soucie pas des rendus en place, ce qui rend vos jours et vos nuits de codage Alors pourquoi même allouer de la mémoire à un objet pointeur vers rendu? Même un membre de pointeur qui n'est jamais utilisé prend encore de la mémoire. Inversez donc la direction du pointeur: l'instance liée au rendu fait référence au modèle de données (qui peut être ou inclure votre nœud de graphe de scène), PAS vice versa. Et si vous voulez un moyen simple de parcourir votre liste de contrôleurs tout en ayant accès à la vue associée, utilisez un dictionnaire / table de hachage, qui approche le temps d'accès en lecture O (1). De cette façon, il n'y a pas de contamination, et votre logique de simulation ne se soucie pas des rendus en place, ce qui rend vos jours et vos nuits de codage Alors pourquoi même allouer de la mémoire à un objet pointeur vers rendu? Même un membre de pointeur qui n'est jamais utilisé prend encore de la mémoire. Inversez donc la direction du pointeur: l'instance liée au rendu fait référence au modèle de données (qui peut être ou inclure votre nœud de graphe de scène), PAS vice versa. Et si vous voulez un moyen simple de parcourir votre liste de contrôleurs tout en ayant accès à la vue associée, utilisez un dictionnaire / table de hachage, qui approche le temps d'accès en lecture O (1). De cette façon, il n'y a pas de contamination, et votre logique de simulation ne se soucie pas des rendus en place, ce qui rend vos jours et vos nuits de codage Et si vous voulez un moyen simple de parcourir votre liste de contrôleurs tout en ayant accès à la vue associée, utilisez un dictionnaire / table de hachage, qui approche le temps d'accès en lecture O (1). De cette façon, il n'y a pas de contamination, et votre logique de simulation ne se soucie pas des rendus en place, ce qui rend vos jours et vos nuits de codage Et si vous voulez un moyen simple de parcourir votre liste de contrôleurs tout en ayant accès à la vue associée, utilisez un dictionnaire / table de hachage, qui approche le temps d'accès en lecture O (1). De cette façon, il n'y a pas de contamination, et votre logique de simulation ne se soucie pas des rendus en place, ce qui rend vos jours et vos nuits de codagemondes plus facile.

En ce qui concerne l'abattage, reportez-vous à ce qui précède. L'abattage des zones d'intérêt est un concept logique de simulation. Autrement dit, vous ne traitez pas le monde en dehors de cette zone (généralement encadrée, circulaire ou sphérique). Cela a lieu dans la boucle principale du contrôleur / jeu, avant le rendu. En revanche, l'abattage tronconique est purement lié au rendu. Alors oubliez l'abattage maintenant. Cela n'a rien à voir avec les graphiques de scène, et en vous concentrant dessus, vous masquerez le véritable objectif de ce que vous essayez d'atteindre.

Une note finale ...

J'ai l'impression que vous venez d'un arrière-plan Flash (en particulier AS3), étant donné tous les détails sur le rendu inclus ici. Oui, le paradigme Flash Stage / DisplayObject inclut toute la logique de rendu dans le cadre du scénario. Mais Flash fait beaucoup d'hypothèses que vous ne voulez pas nécessairement faire. Pour un moteur de jeu à part entière, il est préférable de ne pas mélanger les deux, pour des raisons de performances, de commodité et de contrôle de la complexité du code via un SoC approprié .

Ingénieur
la source
1
Merci Nick. Je suis en fait un animateur 3D (vrai 3D pas flash) devenu programmeur, donc j'ai tendance à penser en termes de graphisme. Si cela ne suffit pas, j'ai commencé à Java et je me suis éloigné de la mentalité «tout doit être un objet» inculquée dans ce langage. Vous m'avez convaincu que le graphique de la scène doit être séparé du code de rendu et d'élimination, maintenant mes engrenages tournent exactement sur la façon dont cela devrait être accompli. Je pense à traiter le moteur de rendu comme son propre système distinct qui référence le graphe de scène pour les données de transformation, etc.
Cody Smith
1
@CodySmith, content que cela ait aidé. Plug sans vergogne, mais je maintiens un cadre qui est tout sur SoC / MVC. Ce faisant, je suis tombé sur le camp le plus traditionnel de l'industrie qui insiste sur le fait que tout devrait être dans un objet central et monolithique. Mais même ils vous diraient généralement - gardez votre rendu séparé de votre graphique de scène. SoC / SRP est quelque chose que je ne saurais trop insister - ne mélangez jamais plus de logique en une seule classe que vous n'en avez besoin. Je préconiserais même des chaînes d'héritage OO complexes plutôt que des logiques mixtes dans la même classe, si vous me mettez un pistolet dans la tête!
Ingénieur
Non j'aime le concept. Et à droite, c'est la première mention de SoC que j'ai vue depuis des années sur la conception de jeux. Merci encore.
Cody Smith
@CodySmith Réflexion rapide en parcourant à nouveau ce sujet. En général, il est bon de garder les choses découplées. Pour différents types d'objets modèle contrôleur dans votre base de code qui subissent de rendu, cependant, il est très bien pour vous de garder les collections de Renderables ( ce qui est une interface ou classe abstraite) en interne pour les principaux objets modèle contrôleur. Les entités ou les éléments d'interface utilisateur en sont de bons exemples. Ainsi, vous pouvez accéder rapidement uniquement aux moteurs de rendu pertinents pour cet objet principal particulier - sans les détails d'implémentation qui contamineraient la classe d'entité, d'où l'utilisation d'interfaces.
Ingénieur
@CodySmith L'avantage est clair avec les entités, ce qui pourrait par exemple. avoir des représentations à la fois dans la fenêtre d'affichage du monde et sur une mini-carte. D'où la collection. Vous pouvez également autoriser un seul emplacement de rendu pour chaque objet modèle-contrôleur, en interne à cet objet. Mais gardez l'interface générale! Pas de détails - juste Renderer.
Ingénieur