Contenu du renducontact(at)guillaumelevieux.com :: « Home

Maxime Perault

Programmeur Enjmin

Lien du github: https://github.com/maxime-perault/ProceduralEngine

Lien vidéo youtube: https://www.youtube.com/watch?v=9h1zyKtJZXs&feature=youtu.be

Ce rendu comporte aussi la partie de LD procedural du cours de Guillaume Levieux. A noter que c'est un moteur OpenGl from scratch qui ne reprend rien du moteur donné en exemple, tout à été recodé. Le programme tourne sur mon PC portable à 400FPS, c'est un PC portable moderne mais pas non plus une machine de guerre, donc n’hésitez pas à l'essayer sur la votre. Toutes les fonctionnalités du moteur/jeu sont décrites ci-dessous:

TEXTURING :

Atlas de textures :

J’utilise un atlas de textures, il n’y a donc qu’une image (png) de chargée pour tout le jeu.
J’ai par ailleurs utilisé un unstitcher minecraft pour former mon propre atlas grâce aux texture packs trouvés sur le net, car depuis la 1.4.5 les textures ne sont plus stockées dans un seul « terrain.png » mais sont indépendantes les unes des autres puis sont générées dans un terrain.png en runtime, surement pour éviter les problèmes de bleeding et de décalage de pixels dus au mipmapping et à l’utilisation d’un filtre anisotropique.

Mipmapping :

Au départ j’ai utilisé le mipmapping standard d’opengl, mais j’avais du bleeding à cause du fait que les pixels limitrophes correspondaient à des textures totalement différentes, une des solutions que j’avais sur le moment était de décaler mes offsets d’uv d’un pixel vers le centre de la texture, mais on perdait du coup un pixel de chaque coté, ce qui finalement avait peu d’intérêt graphiquement parlant.
J’ai donc injecté mes propres niveaux de mipmapping (7 au total) directement dans le moteur pour éviter ces soucis de bleeding qui restaient présents malgrès l’utilisation de GL_NEAREST_MIPMAP_NEAREST en paramètre lors de l’import de la texture. (Il n’y a donc pas exactement 1 seule texture de chargée mais bien N fois le nombre de niveaux de texture au début du jeu).

Transparence :

La transparence est gérée par un discard dans le fragment shader. Dans notre contexte de « minecraft like », cette technique reste suffisante.

LIGHTING :

Le moteur gère une lumière lambertienne simple ainsi que la possibilité d’ajouter ou nom de la lumière spéculaire (Blinn) sur chaque type de bloc.

OMBRES :

Shadowmap :

J’utilise (pour les ombres) une shadowmap (rendu du depth buffer dans un FBO) dont la shadowbox (boite définissant le rendu de la scène à partir d’une vue orthographique) est adaptée en temps réel sur le view frustum utilisé par le joueur, ceci étant fait pour minimiser la taille de la shadowmap et avoir des ombres plus précises. La shadowmap utilise sont propre pipeline de shaders pour un rendu minimaliste utilisant le moins de performance possible, cette technique demandant de rendre la scène 2 fois.

PCF :

Pour régler ce manque de précision qui reste tout de même présent, j’ai utilisé un PCF (percentage closer filtering) en entrée de mon fragment shader principal.

GESTION DU TEXTE :

Pour le texte, j’ai codé mon propre parser/lexer de .fnt qui recupère les informations de placement de texte contenu dans le .fnt et les interprète pour récupérer les lettres (sous forme de .png) dans l’atlas de texture stockant ces dernières. J’ai donc un contrôle total sur l’espacement des lettres, la taille du texte et sur toute forme d’effet de texte.
Je peux par ailleurs utiliser n’importe quelle font existante. Pour se faire, je convertis le .ttf d’une font « moderne » en .fnt via une application java externe disponible ici: https://libgdx.badlogicgames.com/tools.html (l’application se nommant « Hiero »)
Petite précision : un affichage complet de texte n’est pas équivalent à un seul VAO mais à plusieurs VAOs, car certaines informations restent fixes quand d’autres peuvent bouger. Ainsi, si le joueur ne bouge pas, seuls les caracteres liés à son nombre de FPS seront régénérés (réécriture du VAO corréspondant à « 60 ») et ce, seulement si les FPS changent.

MODE DEBUG

Un mode debug est activable ou désactivable en appuyant sur TAB avec les axes XYZ sur le crosshair du joueur et les FPS, position du joueur, chunk courant en haut à gauche (il n’y a pas le même nombre d’informations que dans Minecraft mais le format est le même).

PHYSIQUE ET COLLISIONS

La physique de saut du joueur est tirée d’une conférence GDC sur le saut dans les jeux vidéos :
https://www.youtube.com/watch?v=hG9SzQxaCm8

L’idée étant que lorsque l’on prend l’équation suivante :

1/2 * g * t^2 + v0 * t + p0

On a dans l’équation classique g et v0 qui sont des constantes plus ou moins choisies au hasard, en général on mets 9.81 pour g si l’on veut la terre comme référentiel et v0 à 10 puisque l’on a estimé que ca « devrait suffire ». Mais pour placer son LD, ce procédé laisse trop d’imprécisions, tant sur la durée totale du saut que sur le pic d’altitude atteint.
Pour remédier à ca, on a donc :

v0 = (2h)/th
g = (-2h)/(th^2)

« th » étant l’instant t ou le pic d’altitude du saut est atteint et h ce fameux pic d’altitude.
En utilisant cette méthode j’ai donc définit un saut qui atteint son pic en 0.25sec à une altitude de 1.2 bloc. (c’est une parabole donc on retombe aussi en 0.25sec). A l’arrivée, ca nous donne un saut quasiment similaire à celui de Minecraft et facilement réglable.
Pour les collisions, j’utilise un simple « bounding box test » et vérifie donc si lorsque je me déplace dans un sens, un sommet de l’un des cubes rencontrés par le joueur chevauche l’un des siens. Si un axe est bloquant mais pas l’autre le déplacement est quand même validé mais l’axe bloquant vaudra 0, ce qui donne cet effet de glissement présent dans beaucoup de jeux-vidéos.

LE PICKING:

On peut placer ou supprimer un bloc via un simple test en raytracing qui boucle sur une distance maximum définie à 3 blocs, la verification se faisant à un intervalle assez petit pour éviter de pointer sur le mauvais bloc.
Une fois le bloc définit, je récupère la face pointée par le joueur pour pouvoir placer un bloc à partir de celle-ci si nécessaire (exactement comme dans minecraft). Le bloc ciblé est highlighté par un effet fil de fer. (cet effet fil de fer est un simple VAO dessiné en wireless qui est chargé au début du jeu et se superpose avec le bloc ciblé).
Le picking fonctionne évidemment que ce soit en FPS ou en TPS.

LES CHUNKS

Les chunks générés sont entièrements creux et se comparent les uns les autres pour savoir si leurs limites doivent être ou non effacées.
Chaque chunk est un VAO (il y a aussi de l’indexing) ce qui augmente drastiquement les performances du jeu mais implique de régénérer entièrement le chunk lors de la modification d’un bloc (picking). Lorsqu’un chunk est régénéré, je vérifie entre-autre si cela modifie aussi les chunks voisins.
Par exemple, si je pick un bloc limitrophe d’un chunk, mes chunks étant creux, il faut que je regenere aussi le chunk voisin, sinon, graphiquement, on aurait un trou infini dans le vide bien que le bloc soit physiquement présent.

GENERATION INFINIE ET LD PROCEDURAL:

Génération infinie :

Pour ce qui est de la génération infinie, lorsque le joueur avance, s’il dépasse le chunk courant (chunk central), un thread est lancé et calcule en fonction de l’axe de dépassement, la nouvelle ligne de chunks.
Pour se faire, le thread fait un roulement sur l’ordre des chunks, écrasant la ligne de chunks devant disparaitre, puis la nouvelle ligne est calculée et stockée dans la Factory se chargeant de créer et de dispatcher les vaos, le thread se termine ici.
En sortie de thread, on a donc la même carte avec une « ligne » de chunks en moins.
A chaque frame, je lance dans ma factory de VAOs une fonction qui verifie s’il y a des VAOs de stockés mais pas encore récupérés dans ma carte de jeu, s’il y a bien des VAOs de stockés, j’en charge maximum 8 dans ma carte de jeu courante et ainsi de suite à chaque frame jusqu’à ce qu’il n’en reste aucun.
Par exemple pour une carte de 16*16*16, en sortie de thread la ligne la plus éloignée derrière nous (1*16*16) disparait, la ligne la plus éloignée devant nous (1*16*16) est calculée puis stockée et attends d’être chargée en jeu, on a donc 256 chunks en attente de chargement, à chaque frame on load 8 chunks dans la carte courante (ce qui demandera donc 32 frames).
Cette méthode permet donc de charger progressivement un nombre conséquent/infini de chunks sans pour autant perdre des fps, à noter que créer/modifer des VAOs dans un thread autre que le thread principal est impossible à moins de stocker l’adresse des VAOs coté CPU, c’est une fonctionnalité proposée par OpenGl mais très déconseillée car gourmande en performances.
Maintenant que j’ai expliqué la génération infinie. Une petite explication vis-à-vis de la regénération des chunks lors du picking s’impose. En effet, si je pick un chunk pendant que je génére de nouveaux chunks car le joueur viens de dépasser le chunk courant, le chunk pické sera écrasé par la génération ou pire il sera prioritaire.
Pour éviter cela, lorsque je pick un chunk, je le place dans cette fameuse liste de chunks à générer dans la carte courante, lorsque je le place, je verifie si l’ID de ce VAO existe déjà, si oui, je le remplace en gardant sa Matrice Model et je le fais passer devant en priorité (premier élément à générer à la prochaine frame).

LD procédural :

En terme de forme, la carte est générée par un bruit de perlin sur 3 octaves différentes pour le réalisme avec une puissance dépendante de l’altitude, ce qui nous donne des reliefs alpins avec quelques iles flottantes.
Pour ce qui est des blocs, je définis d’abord si un bloc est plein(terre) ou vide(air), puis s’il est au sommet d’une pile, le bloc devient de l’herbe.
Il y a aussi un niveau de rareté de blocs en fonction de l’altitude (pour l’instant il y en a peu mais c’est facilement incrémentable), on a donc plus de chance d’avoir de la pierre et du charbon sur une altitude basse que l’inverse, le charbon ne peut être trouvé en hauteur mais la pierre si pour donner du corps aux iles flottantes, les iles flottantes uniquement constituées de terre étant peu réalistes.
A partir d’une altitude assez basse, la terre devient du sable et de l’eau apparait (pour l’instant c’est juste un plan bleu transparent).
A noter que 3 types d’arbres sont générés mais ne peuvent être générés sur une altitude inférieure à celle de l’eau. Beaucoup de solutions pour générer ces arbres pouvaient être choisies, la taille de mes arbres étant fixe, et un chunk étant plus de 2 fois plus haut que mon arbre, je n’ai pas eu à calculer la génération cross Chunk, tous les blocs d’un arbre font donc partie intégrante du même chunk, ce qui allège entre autre le calcul de ces derniers.
Les arbres sont calculés par couches, partant de la plus basse à la plus haute, il serait donc aisé de calculer une croissance en fonction du temps (fonctionnalité à l’heure actuelle non implémentée, mais la génération est pensée pour).