Partage
  • Partager sur Facebook
  • Partager sur Twitter

[Jeu/Engine] ALAGEngine - 2.5D Shading Engine

Isometric 2.5D Shading Engine in C++/SFML/OpenGL

    22 février 2018 à 0:49:32

    Bonjour à tous,

    Tout d'abord je me présente: je m'appelle Grégoire, je suis actuellement étudiant en thèse de maths. J'ai passé ma jeunesse à développer des jeux amateurs, principalement en C et C++ (oui je viens de l'époque du siteduzero, d'ailleurs je ne suis plus venu par ici depuis que ça a changé de nom je pense). Peut-être que certains vieux briscards parmi vous se souviennent d'Holyspirit que je développais il y a quelques années:

    Ensuite la motivation m'a quitté et j'ai mis de côté ce hobby (principalement pour produire des maths). Cependant, la nostalgie du développement de jeux me touchant de plus en plus, j'ai décidé de m'y remettre (ce qui n'est pas forcément très bon pour ma productivité de papiers). Donc me voici pour parler de mon nouveau projet !

    En savoir plus sur le projet

    Genèse

    Il y a 6 ans, j'avais commencé à travailler sur un début de 2.5D shading engine (i.e. rendu d'asset 3D prérendu old-school en utilisant des techniques d'éclairage modernes) avec la SFML et OpenGL, en C++, qui ressemblait à quelque chose comme ça:

    Afin de me remettre au développement de jeux, j'ai décidé de commencer d'abord par développer un nouveau moteur d'éclairage 2.5D, toujours avec la SFML et OpenGL. Je suis donc ici pour vous en parler, vous montrer un peu ce que je bricole et si possible avoir des retours et idées pour améliorer tout ça. Je compte aussi me servir de ce topic un peu comme un blog de développement, expliquant dans différents messages les techniques que j'ai mises en place pour arriver au résultat, comme je faisais avec Holyspirit (n'hésitez pas à me demander l'un ou l'autre point que vous auriez envie que j'explique plus en détails!).

    Généralités et avancement

    Fiche technique

    • C++
    • SFML/OpenGL
    • Deferred Rendering
    • PBR shading
    • Isometric/2.5D assets

    Avant de me lancer dans des explications plus techniques, afin d'attirer votre attention puisque tout le monde aime les belles images, voici ce que ça donne pour le moment:

    Et en mouvement
    https://perso.uclouvain.be/gregoire.naisse/files/ALAGEngine.gif

    Concept de base

    L'idée est très simple (et pas original : il y a plusieurs exemples sur youtube, sinon Pillars of Eternity utilise un concept similaire) et est telle que suit : beaucoup de moteurs de rendus 3D utilisent une technique qu'on appelle "deferred shading" qui consiste à reporter le plus loin possible le calcul de l'éclairage de la scène. Concrètement, ça se fait en rendant toute la géométrie de la scène dans différents buffers qui contiennent l'information des fragments (i.e. pixels à l'écran): leur couleur diffuse, l'orientation de la surface, le matériau et la position dans la scène. Ensuite, basé sur ces informations-là, le shader d'éclairage calcule l'illumination de chaque pixel affiché à l'écran. Le gros avantage de cette technique est que le calcul d'éclairage n'a besoin de se faire qu'une seule fois par pixel, au lieu d'une fois par pixel par objet (on n'a pas besoin de calculer l'éclairage d'un pixel caché par un autre objet, en gros). Je vous invite à consulter ce tutoriel pour plus d'explications.
    Et maintenant, tout ce qu'il nous reste à faire c'est mélanger ces techniques d'éclairage avec des objets high-poly prérendus type isométrique 2.5D, où au lieu de pré-rendre un modèle déjà éclairé, on pré-rend les 4 passes (couleur, orientation, matériaux, hauteur). Par exemple:


    Et donc dans la scène on obtient:
    Albedo:
    Nom : PBR0.png Affichages : 84 Taille : 1,94 Mo
    Normal:
    Nom : PBR1.jpg Affichages : 85 Taille : 121,5 Ko
    Heightmap:
    Nom : PBR2.png Affichages : 89 Taille : 1,09 Mo
    Material:
    Nom : PBR3.png Affichages : 92 Taille : 305,0 Ko

    Où j'utilise en plus le depth buffer d'OpenGl pour déterminer si un pixel d'un sprite est devant ou derrière un autre afin de le cacher, permettant d'intersecter des objets 2.5D comme si ils étaient en 3D.

    Ensuite, en utilisant ces 4 buffers, on peut éclairer la scène en fonction des lumières, ajouter du SSAO (Screen Space Ambient Occlusion, càd de l'ombrage basé sur la géométrie de la scène pour rendre les coins plus sombres), du bloom, etc. Afin de rendre le moteur un peu plus moderne, j'ai décidé d'implémenter des méthodes de shading dites PBR (i.e. Physically Based Rendering) qui sont des techniques utilisant des équations plus avancées que Blinn-Phong, inspirées de la physique, développées principalement par Disney (pour les films d'animations) et Epic Games (pour l'UT 4), je ne m'abuse. Elles permettent un rendu assez réaliste et rendent la création d'asset plus naturel (les matériaux sont juste déterminé par un niveau de rugosité et de métallicité, assez intuitif):

    Ce que le moteur fait actuellement

    • Calcul de la géométrie de la scène en tenant compte de la heightmap pour calculer l'intersection d'objets 2.5D en temps réel.
    • Éclairage de la scène en utilisant des techniques PBR (i.e. Physically Based Rendering).
    • Calcul de projection d'ombres pour lumière directionnel (type soleil) basé sur la géométrie visible.
    • Calcul de projection d'ombres dynamiques pour lumières omnidirectionnelles en utilisant de la géométrie 3D basique invisible ajoutée à la scène
    • SSAO.
    • SSR.
    • Bloom.
    • Optimisation de rendu de scène complexes dont la majeure partie est statique.

    Objectifs

    La suite à court terme
    Il reste encore beaucoup de travail pour améliorer l'architecture globale du moteur, ajouter des fonctionnalités de base et surtout l'optimisation (j'ai déjà des idées que je vais implémenter sous peu dont une en cours d'implémentation, je vous tiens au courant). Je compte aussi ajouter bien évidemment des objets animés. Ensuite, je voudrais aussi développer un shader pour rendre l'eau, avec si possible détection de la géométrie pour ajouter de la mousse aux bords. Je voudrais aussi ajouter la possibilité de faire de la pluie, qui humidifierait les zones exposées (ça devrait être facile en utilisant la même technologie que pour la projection des ombres directionnelles et changer les propriétés de type "matériau" du fragment exposé).

    La suite à long terme
    Dans l'avenir, je souhaiterais utiliser le moteur pour un projet de jeu indie open-source (non pas Holyspirit 2 :-°). Mais je ne me sens pas encore prêt à communiquer plus là-dessus.

    Le projet et son originalité

    Comme mentionné précédemment, le projet en soit n'est pas original (on peut retrouver des vidéos sur youtube de moteurs "similaires" en 2009) et des technologies semblables ont déjà été utilisées dans des jeux commerciaux (Pillars of Eternity). Cependant, je pense être le seul à y mélanger de la "technologie PBR". Et même si ce n'est pas le cas, ça me permet d'apprendre beaucoup sur les technologies actuelles et de me remettre un peu à jour.

    Je tiens à mentionner que la plupart des techniques d'éclairages sont inspirées de l'excellente série de tutoriels que voici : https://learnopengl.com/

    Et pourquoi pas de la 3D ?

    La première réponse qui me vient à l'esprit est très simple: je suis incapable de faire des modèles 3D low-poly. Par contre, faire du high poly avec des mesh pas très propres, ça me va. Donc ça me permet en gros de pouvoir produire mes assets moi même.
    De plus, j'aime beaucoup le petit côté old-school que donne la 2.5D iso.
    Finalement, c'est beaucoup de fun, et même si niveau performances ce n'est pas forcément vraiment plus intéressant que de la 3D bien faite, ça me permet de mettre en place des optimisations intéressantes.

    Et les sources dans tout ça ?
    Pour le moment, je préfère ne pas trop les distribuer car beaucoup de choses sont encore trop brouillonnes je pense. Cependant, elles seront disponible librement et je peux déjà vous les envoyer sur demande.

    -
    Edité par Gregouar 2 mars 2018 à 13:44:26

    • Partager sur Facebook
    • Partager sur Twitter
      22 février 2018 à 2:20:47

      C’est pas tous les jours qu’on voit des projets comme ça. Ni toutes les semaines ou tous les mois. o_O

      Franchement GG. Juste GG.

      • Partager sur Facebook
      • Partager sur Twitter
        28 février 2018 à 22:38:06

        Haha, merci mais ce n'est pas si difficile ou ambitieux que ça tu sais, il faut juste se renseigner un peu sur ce qui se fait actuellement. =)

        Pour le moment, je fais des tests avec la Screen Space Reflection:

        Nom : SSRtorusred.png Affichages : 3 Taille : 207,7 KoNom : toruswithoutSSR.png Affichages : 1 Taille : 200,4 Ko

        Avec et sans SSR; et un autre exemple sur une scène complète où j'ai mis le sol comme réfléchissant pour tester:

        Nom : screenshot1SSRred.jpg Affichages : 3 Taille : 163,5 Ko

        Ça rend pas mal, mais bien évidemment étant une technique qui utilise du raytracing, c'est assez lent. Cela restera donc une option pour les grosses machines.

        Par ailleurs, j'ai mis au point une optimisation pour afficher les scènes contenant beaucoup d'entités. Le problème, le voici:
        Nom : tori_invasionbefore.png Affichages : 50 Taille : 1,85 Mo
        J'ai placé quelque chose comme 1000 tores dans la scène et évidemment, rendre toute cette géométrie dans les 4 buffers 2 fois (une passe pour l'opaque et une passe pour l'alpha, ici l'anticrénelage), ça consomme beaucoup de performances (on peut voir que je suis à moins de 10 images par seconde) ! Peut-être que certain d'entre vous me diront d'utiliser des FBO et c'est réglé. Sauf que c'est juste une simulation pour tester et qu'en vrai je voudrais pouvoir faire des scènes complexes avec beaucoup d'objets différents. De plus, je ne suis pas sûr que ça fonctionnerait vraiment bien étant donné que je dois rendre chaque sprite 4 fois (il faudrait alors faire chaque passe à la suite, si je ne m'abuse).

        Alors, comment faire ? o_O

        Heureusement, en général, la scène est principalement composée d'éléments statiques, qui ne bougent pas ! Dès lors, pourquoi se fatiguer à refaire le rendu de leur géométrie à chaque fois, recalculer le depthbuffer, etc, alors qu'une seule fois suffirait ? Autant faire une fois le rendu, le garder en mémoire, puis dessiner par dessus les objets dynamiques qui bougent. Et, pourquoi pas, refaire le rendu si jamais un élément qui devrait être statique aurait bougé, non ? D'autant plus que tout l'éclairage est déjà "deferred" et est donc calculé que sur le rendu final !

        Bien sûr, on ne peut pas se permettre de faire le rendu de toute la map, ça serait un peu trop gourmand en mémoire (je pense). Dès lors, j'ai décidé de couper mon écran en "plein" (pas trop non plus) de sections carrées, que je pré-rends. Ensuite, il suffit de vérifier si un objet statique a bougé dedans, et si oui refaire le rendu que de ce morceau là de l'écran (en utilisant par exemple le stencil buffer). Bien sûr, ça ne marche pas si on bouge la caméra ! On doit alors recalculer le rendu tant qu'on bouge la caméra et ça donne un résultat bien saccadé... Ma solution est donc de travailler avec "un écran virtuel", un buffer, un peu plus grand (d'une section en faite) que l'écran réel. Dès lors, je peux le faire bouger avec la caméra, en ne recalculant éventuellement que la portion qu'il faut si on arrive à la limite (on copie les buffers, on décale d'une section et on recalcule juste la bande qu'il manque).

        Maintenant, grâce à ça, je peux rendre ma scène et obtenir les même performances que sans aucun tore affiché à l'écran (60 FPS sur mon portable pour le moment). Avec uniquement une baisse quand on bouge la caméra, avec juste une frame qui descend à l'équivalent de 20-25 FPS (juste celle où on met à jour la bande à afficher en gros).

        Je pense que cette optimisation va être l'un des avantages principaux du fait de travailler avec un moteur isométrique.

        -
        Edité par Gregouar 1 mars 2018 à 10:05:45

        • Partager sur Facebook
        • Partager sur Twitter
          6 mars 2018 à 14:39:03

          Ça rend vraiment bien !

          Merci pour ces explications, c'est hyper intéressant :)

          • Partager sur Facebook
          • Partager sur Twitter
            7 mars 2018 à 18:31:46

            Depuis le temps que j'ai ça en tête, je me suis enfin jeté... à l'eau ! :pirate:



            Ca va encore demander beaucoup de tweaks et de tests pour arriver au résultat que je désire, mais ça commence quand-même à ressembler à quelque chose.

            Pour les amateurs de technique, c'est du bruit de Perlin mélangé à un sinus ici (il faut que je change pour faire des vagues plus réalistes). Et en gros, je ne fais que générer une heightmap, normalmap (facile de calculer les normales en calculant les dérivées du bruit), albedo (pour la mousse) et material (pour la mousse aussi). Après, c'est juste la magie du parallax et des équations PBR qui étaient déjà dans le moteur (en fait l'eau passe exactement par le même pipeline de rendu que tout le reste).

            -
            Edité par Gregouar 7 mars 2018 à 18:32:21

            • Partager sur Facebook
            • Partager sur Twitter
              16 mars 2018 à 1:00:48

              Quelques nouveautés depuis la dernière fois.

              Anticrénelage, objets transparents et SSAO

              J'avais des soucis d'anticrénelage et de gestion de l'ambient occlusion avec les objets transparents. l'anticrénelage des objets est contenu dans la couche de rendu alpha. Dès lors, est-ce qu'il faut l'assombrir avec l'occlusion ambiante ou non ? Si on assombrit les objets transparents, alors c'est étrange car un objet semi-transparent devient foncé à cause d'une ombre derrière lui. D'un autre côté, si on ne le fait pas, alors l'anti-crénelage ressort illuminé.

              Voici une image pour illustrer le problème:
              Nom : issues.png Affichages : 144 Taille : 352,3 Ko

              A gauche on n'a l'anticrénelage qui vient prendre la place des fragments d'eau car ils sont au dessus, en haut à droite ce sont les artefacts qu'on a si on dessine l'SSAO en dessous des fragments semi-transparents et en bas à droite si on dessine l'SSAO par dessus.

              Si je viens en parler, c'est parce que j'ai trouvé une solution (partielle): il y a un moyen de savoir si un fragment est semi-transparent à cause de l'anticrénelage ou non et c'est... de regarder l'opacité du fragment dans la heightmap. En effet, l'eau et les objets que je dessine semi-transparents ont quand-même leur heightmap complètement opaque, alors que l'anticrénelage est donné par le logiciel de rendu 3D, qui met aussi de l'anticrénelage sur les heightmaps qu'il produit.
              Alors ça, ça résout facilement le problème de SSAO (on applique SSAO si et seulement pixel opaque ou depth transparent).
              Pour l'eau, c'est un peu plus compliqué car on n'a toujours qu'un seul channel de rendu pour les pixels transparents... mais l'idée est de donner priorité aux pixels transparent ayant un depth non transparent (pour le moment là façon dont je le fais n'est pas optimale car ne donne priorité que aux objets transparents dynamique par rapport aux statiques, simplement dans la façon dont je copie le depthBuffer du buffer alpha static vers le buffer alpha dynamique).

              Voici le résultat !
              Nom : problemsSolved.png Affichages : 142 Taille : 165,4 Ko
              Alors bien sûr on perd l'AA sur la tour au dessus de l'eau, mais c'est le prix à payer, on ne peut pas faire mieux sans manger méchamment dans les perfs. De plus, cela pourra probablement être résolu en introduisant une pass d'anti-aliasing type FXAA ou mieux (celle-ci consiste en une détection de bord et lissage de ceux-ci en post-traitement).

              Gerstner waves

              J'ai enfin implémenté mes vagues de type Gerstner, la difficulté a été que la fonction n'est pas présentée de façon traditionnel par y = f(x) mais par un mouvement circulaire de particules:


              Source: https://en.wikipedia.org/wiki/Trochoidal_wave

              En général, cette description est très pratique car on peut interpréter les sommets d'un mesh comme des particules qu'on bouge en fonction de ces mouvement circulaires... sauf que moi je veux générer une heightmap, et donc il me faut une hauteur à chaque point ! Ma solution a donc été de prendre un échantillon de N points uniformément réparti sur l’intervalle unité, ensuite je déplace chacun de ces points en fonction de mon mouvement circulaire (je regarde le mouvement de N particules d'eau si vous voulez), en m'arrangeant pour faire un tour complet entre 0 et 1 (comme ça, j'ai un seul mouvement de vague). Ensuite, il me suffit d’interpoler entre ces points, avec une interpolation linéaire ça donne ça:
              Nom : wave15.png Affichages : 62 Taille : 778 octets

              Dans les fait, dans mon shader j'utilise une interpolation quadratique pour avoir un changement de normal plus lisse (linéaire). Ensuite on peut juste jouer avec la forme du mouvement circulaire, le rendant ellipsoïdale, pour avoir des vagues plus ou moins pointues.

              Au passage, j'en ai profité pour rendre l'eau plus ou moins opaque en fonction de sa profondeur (puisque l'eau est rendu dans la pass d'alpha, tout ce que j'ai à faire est lire la hauteur du pixel dans le buffer de la pass de géométrie opaque et comparer).

              Edge smoothing

              Par ailleurs, j'ai décidé de travailler sur l'aliasing qui apparait quand on fait s'intersecter deux objets. En effet, un fragment réussit ou non le test du depthbuffer et en fonction est pris ou non, ce qui peut donner de l'aliasing assez dégueulasse quand les matériaux sont fort différents (surtout à cause de la précision limitée de mes heightmaps). Une solution est probablement de simplement mettre en place un shader de FXAA  (Fast approXimate Anti-Aliasing) ou mieux (SMAA T2X ?), ce qui devrait rendre le rendu beaucoup plus lisse ; je ne l'ai pas encore fait mais j'envisage de le faire très prochainement (en attendant on peut l'activer avec le driver nVidia). Le gros avantage c'est que ça va aussi s'occuper de lisser le specular aliasing (aliasing du à grand différence d'éclairage, par exemple un objet pointu ou un bord d'un objet lisse peut réfléter beaucoup (trop ?) de lumière par rapport à son voisin). Par contre le désavantage c'est que étant juste un post-traitement de l'image il peut avoir des effets indésirables. De plus, je ne suis pas sûr qu'il va vraiment améliorer l'aliasing étrange qui apparait à cause du fait que mes heightmaps sont limitées en précisions.

              Du coup, j'ai décidé de tenter une autre technique, basée sur ce que j'avais fait juste avant avec l'eau qui devient transparente en fonction de la géométrie en dessous: pendant la pass d'alpha, si j'ai un pixel opaque qui est très proche d'un autre mais juste en dessous, je viens le redessiner par dessus, avec transparence. C'est une gros approximation étant donné que des pixels qui devraient être cachés viennent se mettre par dessus... mais ça se fait à un tellement petit niveau que je pense que ça passe assez bien. Le gros avantage est que l'impacte sur les performances est quasi nul et que ça ne touche que l'endroit où deux objets s'intersectent. Voici le résultat:
              Nom : smoothEdges.png Affichages : 64 Taille : 234,1 Ko
              Haut-gauche: sans AA, sans SSAO ; Haut-droite: sans AA, avec SSAO
              Bas-gauche: avec AA, sans SSAO ; Bas-droite: avec AA, avec SSAO

              Screen space foam rendering

              Ensuite, j'ai aussi bossé sur le rendu de la mousse pour l'eau:


              Une vidéo youtube du rendu de la mousse en action !

              Je vous présente mon algorithme de Screen Space Foam Rendering. L'idée ? C'est simple, en même temps que je génère mes heightmap, normalmap et co pour l'eau, je génère en plus une "velocity map" qui contient les vecteurs vitesses du mouvement de mes particules d'eau. Ensuite, au moment du rendu de l'eau, j'ai juste besoin de faire un léger raytracing pour voir si je prédicte une collision avec le décor, et en fonction de la distance je génère plus ou moins de mousse, tout simplement en changeant l'albedo et material. De plus, puisque je génère cette map à ce stade-ci, je peux directement profité de mon bruit de Perlin déjà généré sur place pour introduire de l'aléatoire dans la génération de mousse simplement en perturbant mes vecteurs vitesses.

              J'ai mis du temps à obtenir des résultats qui m'allaient car au début je générais mes vecteurs vitesses basé sur le mouvement des particules dans la fonction de Gerstner. Mais du coup ça mettait de la mousse à l'avant de la vague (logique) mais rien sur le dos puisque leur velocité va vers le haut. De fait, ça ne peut pas bien donner puisque je ne gère pas une véritable simulation de la physique de l'eau. Dès lors j'ai décidé d'introduire des vecteurs vitesses "artificiels" qui pointent vers le bas, dans la direction de la vague, sur son dos. Ca me permet d'utiliser les fragments d'eau de celle-ci pour générer la mousse sensée se produire par le fracas de la vague sur le décor. Le résultat est assez sympa, je pense, et plutôt raisonnable en terme de cout de performances.

              Après on peut jouer sur les couleurs et matériaux:

              Mer des caraïbes:



              Mer de heu... mercure ?

              -
              Edité par Gregouar 16 mars 2018 à 1:26:03

              • Partager sur Facebook
              • Partager sur Twitter
                16 mars 2018 à 11:11:18

                C'est...tout simplement impressionnant.
                • Partager sur Facebook
                • Partager sur Twitter
                /!\ Si je cesse de répondre c'est parce que vous êtes venus poster sans avoir suivi les cours de base sur le sujet. /!\
                  16 avril 2018 à 11:21:54

                  Une petite vidéo pour vous montrer où on est le rendu des vagues qui se jettent sur la plage:
                  https://youtu.be/jwHon2FViTk

                  • Partager sur Facebook
                  • Partager sur Twitter
                    18 avril 2018 à 15:18:14

                    J'ai pas vraiment d'autre mot en tête que GG. Je suis à la fois en admiration devant tous les efforts et en contemplation devant le résultat que ça donne (en voyant le titre du post je m'attendais à voir un screen pourri de jeu sur Game Maker, comme d'hab), là on part vraiment sur un moteur de rendu complet avec toutes les formules de maths qui vont avec.

                    GG.

                    • Partager sur Facebook
                    • Partager sur Twitter

                    zdimension - https://zdimension.fr/

                      19 avril 2018 à 12:37:07

                      Oh c'est loin d'être un moteur complet. J'ai directement été à l'essentiel (un moteur complet devrait intégré beaucoup plus de fonctionnalités de base) et la plupart des maths un peu compliquées sont celles développées par les gars de chez Disney et Epic, comme expliqué sur https://learnopengl.com/. Mais je prends le compliment quand-même. :D
                      • Partager sur Facebook
                      • Partager sur Twitter

                      [Jeu/Engine] ALAGEngine - 2.5D Shading Engine

                      × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                      × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                      • Editeur
                      • Markdown