Gestion de rendu 2D et 3D

Nazara Engine

Le problème exposé dans ce sujet a été résolu.

Bonsoir.

Comme certains le savent déjà, je suis l'auteur d'un moteur de jeu en C++ (Nazara Engine) et actuellement en pleine conception.

Actuellement le moteur est tout à fait capable de gérer la 3D à travers son ECS, voici comment ça fonctionne pour la 3D actuellement:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    Ndk::World world;

    // Caméra
    Ndk::EntityHandle cameraEntity = world.CreateEntity();
    auto& camera = cameraEntity->AddComponent<Ndk::CameraComponent>();
    auto& cameraNode = cameraEntity->AddComponent<Ndk::NodeComponent>();

    // Vaisseau
    NzModelRef spaceshipModel = NzModel::New();
    spaceshipModel->LoadFromFile("resources/Spaceship/spaceship.obj");

    Ndk::EntityHandle spaceshipEntity = world.CreateEntity();
    spaceshipEntity->AddComponent<Ndk::NodeComponent>();
    auto& graphicsComponent = spaceshipEntity->AddComponent<Ndk::GraphicsComponent>();
    graphicsComponent.Attach(spaceshipModel);

    // Boucle de rendu (pseudo-code)
    while (...)
        world.Update(elapsedTime);

Le code est relativement simple à comprendre:

  1. Création du "monde" (contenant des systèmes de base, notamment pour le rendu)
  2. Création et chargement d'un modèle de vaisseau (le maillage, les matériaux/textures associés)
  3. Création d'une entité
  4. Attachement d'un NodeComponent à l'entité (pour lui permettre d'avoir une position/rotation etc.)
  5. Attachement d'un GraphicsComponent à l'entité, sur lequel on attache le modèle de vaisseau.

Et voilà, nous avons une entité affichée par le moteur située quelque part dans le monde, qui sera affichée lors de la mise à jour des systèmes.

L'affichage se fait par le RenderSystem qui va itérer sur les entités possédant un CameraComponent, triées par leur membre "layer index" (permettant de spécifier l'ordre du rendu), ensuite il y a itération sur les entités possédant un GraphicsComponent pour l'affichage.

Ce qui donne en pseudo-code ceci:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    void RenderSystem::OnUpdate(float elapsedTime)
    {
        for (const Ndk::EntityHandle& camera : m_cameras)
        {
            for (const Ndk::EntityHandle& drawable : m_drawables)
                AddToRenderQueue();

            Draw();
        }
    }

Voilà pour le fonctionnement actuel.

Mon problème:

Le problème intervient lorsque je veux rajouter de la 2D à tout ceci.
Alors tout d'abord, le moteur ne fait aucune distinction entre la 2D et la 3D, il ne voit que différentes primitives et s'occupe de les afficher comme on le lui demande.

De fait, un sprite, du texte, un modèle, tout ceci appartient autant à la 3D qu'à la 2D dans Nazara.

Du coup, pour passer de la 3D à la 2D, il n'y a qu'un changement à faire, c'est changer le type de projection, chose qui se fait très simplement (par exemple avec une seconde caméra:)

1
2
3
4
5
    Ndk::EntityHandle view = world.CreateEntity();
    view->AddComponent<Ndk::NodeComponent>();

    auto& viewCamera = view->AddComponent<Ndk::CameraComponent>();
    viewCamera.SetProjectionType(nzProjectionType_Orthogonal); // "Rendu 2D"

Mon véritable problème est que le RenderSystem va traiter les mêmes entités en 2D qu'en 3D, ce n'est certainement pas ce qui est voulu.

Il me faut donc trouver un moyen de séparer les deux, plusieurs solutions s'offrent alors à moi:

  1. Ne rien changer, faire en sorte qu'un second World doive être utilisé pour le rendu 2D, entraînant une séparation totale entre les entités/systèmes, mais pas au niveau des "InstancedRenderable" (sprites, modèles, etc.). Le problème de cette solution est d'être contraignante pour l'utilisateur (mais néanmoins parfaitement logique).
  2. Ajouter un Bitset au niveau du GraphicsComponent permettant d'indiquer les layers de visibilité:

    1
    2
    3
    4
    5
    6
    Ndk::EntityHandle cameraEntity = world.CreateEntity();
    auto& camera = cameraEntity->AddComponent<Ndk::CameraComponent>();
    camera.SetLayer(1);
    
    auto& graphicsComponent = spaceshipEntity->AddComponent<Ndk::GraphicsComponent>();
    graphicsComponent.EnableVisibility(1, true); // bitset en interne
    

    Cette solution ne me plaît pas forcément car elle se base sur le fait que l'indice de couche des caméras ne variera pas, et puis que se passe-t-il quand je veux rajouter une énième caméra, mes entités seront-elles par défaut visibles ou invisible pour celle-ci ? Sans oublier que ça risque d'être chiant à changer par la suite.
    En bref, ça ne facilite pas vraiment la vie de l'utilisateur.

  3. Un autre système de visibilité (comme un champs indépendant visibilité, pour former des groupes de visibilités, ou autre).

Ceci n'est cependant que la première partie de mon problème, voici la seconde:

Le repère

Un autre problème quand on mélange 2D/3D est au niveau des "renderables" générés, par exemple les sprites, problème en image: Image utilisateur

Ici le sprite est en 2D, composé de quatre sommets qui sont positionnés comme l'indique la texture.
Le problème apparaît quand on met ce même sprite en 3D:
Image utilisateur

Les quatre sommets sont toujours positionnés comme l'indique la texture, le problème vient ici du repère qui n'est tout simplement pas le même entre la 2D (Y vers le bas) et la 3D (Y vers le haut).

Ce problème apparaît notamment avec les sprites et les textes, ceux-ci générant eux-même leurs sommets selon un repère arbitraire.

  1. Spécifier le repère à utiliser au niveau du GraphicsComponent:

    1
    textGraphics.SetCoordinateSystem(nzCoordinateSystem_2D);
    

    Le problème de cette méthode est la lourdeur, je pense que ça embêterait plus d'un utilisateur de devoir faire ça pour chaque InstancedRenderable.

  2. Spécifier le repère au niveau du RenderSystem.
    Si on part du principe que les World sont distincts pour la 2D/3D, on peut gérer directement le système de coordonnées au niveau du RenderSystem du monde:

    1
    world.GetSystem<Ndk::RenderSystem>().SetCoordinateSystem(nzCoordinateSystem_2D);
    

Après avoir écrit tout ça, je pense partir dans la logique des mondes distincts (ce qui m'embête un peu) avec le système de coordonnées définit au niveau du RenderSystem, c'est ce qui me semble le plus propre et le moins contraignant.

Mais je voulais quand même vos avis sur l'architecture que j'emprunte, si vous aviez une meilleure idée, ou des questions pour faire avancer la chose, n'hésitez surtout pas !

Merci d'avoir lu.

+0 -0

Je ne comprends pas la possibilité d'avoir deux mondes distincts. Je veux dire, comment ferions nous alors pour les faire intéragir ? J'entends par là avoir une entité 2D réagir avec une entité 3D, puisque si j'ai bien compris l'ECS, seules les entités d'un même monde peuvent réagir entre elle.

+0 -0

Je ne comprends pas la possibilité d'avoir deux mondes distincts. Je veux dire, comment ferions nous alors pour les faire intéragir ? J'entends par là avoir une entité 2D réagir avec une entité 3D, puisque si j'ai bien compris l'ECS, seules les entités d'un même monde peuvent réagir entre elle.

NeDKaM

Justement, quand est-ce qu'une entité 2D doit interagir avec une entité 3D ? C'est justement pour empêcher ça que je propose deux mondes distincts.

+0 -0

Alors évidemment on préfèrera généralement que cela ne puisse pas arriver. Mais il est là le dilemme pour moi, si jamais on trouve un gameplay de ce genre, comment fait-on alors ? Et puis si c'est pour l'empêcher, autant avoir un seul monde que l'on puisse par exemple paramétrer pour choisir un mode d'utilisation des entités. En outre, mettons nous dans l'expleme d'un FPS, on aura l'ECS pour le monde 3D, et la 2D (une GUI, pour ne pas penser plus loin) gérée en surcouche indépendante du monde (mais qui puisse l'altérer). Et au final ça ne gènera pas un utilisateur uniquement 2D.

+0 -0

Alors évidemment on préfèrera généralement que cela ne puisse pas arriver. Mais il est là le dilemme pour moi, si jamais on trouve un gameplay de ce genre, comment fait-on alors ? Et puis si c'est pour l'empêcher, autant avoir un seul monde que l'on puisse par exemple paramétrer pour choisir un mode d'utilisation des entités.

NeDKaM

Je pense que je préfère largement gérer une option une seule fois au niveau du monde qu'une fois pour chaque entité.
Rien ne t'empêche d'avoir de la 2D dans un monde 3D, il te suffit par exemple de coller les sprites/texte au bon endroit (et de les accrocher à la caméra 3D) pour obtenir ce résultat.

En outre, mettons nous dans l'expleme d'un FPS, on aura l'ECS pour le monde 3D, et la 2D (une GUI, pour ne pas penser plus loin) gérée en surcouche indépendante du monde (mais qui puisse l'altérer). Et au final ça ne gènera pas un utilisateur uniquement 2D.

NeDKaM

Attention que le seul endroit où le lien est rompu est bien l'ECS et ses systèmes, ça veut dire que le moteur physique ne considérera pas les entités physique 2D et 3D comme faisant partie du même monde (ce qui serait plus problématique qu'autre chose, sauf exemple au dessus).
Rien ne t'empêche toi au niveau de ton programme de faire fonctionner les deux ensembles (une action sur la GUI peut provoquer tout ce que tu veux en 3D).

+0 -0

Alors évidemment on préfèrera généralement que cela ne puisse pas arriver. Mais il est là le dilemme pour moi, si jamais on trouve un gameplay de ce genre, comment fait-on alors ? Et puis si c'est pour l'empêcher, autant avoir un seul monde que l'on puisse par exemple paramétrer pour choisir un mode d'utilisation des entités.

NeDKaM

Je pense que je préfère largement gérer une option une seule fois au niveau du monde qu'une fois pour chaque entité.

Lynix

Oui c'est cette idée que je soumettais, une seule fois à la création du monde.

+0 -0

J'ai fini par faire ceci:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Ndk::World world2D;
world2D.GetSystem<Ndk::RenderSystem>().SetDefaultBackground(nullptr); // Pour garder le fond 3D
world2D.GetSystem<Ndk::RenderSystem>().SetGlobalUp(-NzVector3f::UnitY()); // Fait pointer Y vers le bas (en 2D)
 
Ndk::EntityHandle view = world2D.CreateEntity();
auto& camComp = view->AddComponent<Ndk::CameraComponent>();
view->AddComponent<Ndk::NodeComponent>();
 
camComp.SetProjectionType(nzProjectionType_Orthogonal); // Projection orthogonale
camComp.SetTarget(&window);
+2 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte