Auteur Sujet: Nazara Terrain - Tutoriel / Fonctionnement  (Lu 1314 fois)

Hors ligne Overdrivr

  • Modérateur
  • Nouveau
  • *****
  • Messages: 3
    • Voir le profil
Nazara Terrain - Tutoriel / Fonctionnement
« le: mars 15, 2013, 10:15:22 am »
Nazara Terrain

Comme son nom l'indique, ce module permet de mettre en place et d'utiliser un terrain dans vos applications Nazara, de manière simple et haut niveau.

Ce module a un comportement dynamique, c'est à dire que le maillage du terrain généré peut être mis à jour en temps réel, de manière à conserver une précision maximale autour de la caméra, et à réduire cette précision au fur et à mesure que la distance à la caméra augmente.

http://www.youtube.com/watch?v=Mm2pXM0SeWA

En tant qu'utilisateur, vous avez accès à de multiples paramètres afin de configurer finement la qualité visuelle du terrain ainsi que ses performances. Une classe de configuration vous aide dans cette tâche, et possède également une configuration par défaut, permettant aux utilisateurs débutants de sauter entièrement cette étape.

Ce module essaye autant que possible de profiter des techniques de génération procédurales, capables de générer à la volée un terrain entier. Cela consiste tout simplement à utiliser des fonctions mathématiques capables de simuler l'aspect d'un terrain, qui renvoient pour deux coordonnées (x,y) la hauteur du terrain en ce point. De telles fonctions sont implémentées dans le module Nazara Noise.

L'utilisation de techniques procédurales n'étant incompatible avec l'utilisation de données éditées manuellement, a terme le système sera capable d'importer des heightmaps qui remplaceront localement les hauteurs calculées procéduralement.

Hello world !
Pour fonctionner correctement, le module a avant tout besoin de connaitre les hauteurs du terrain en tout point. Si vous débutez avec ce module, vous pouvez utiliser une classe prête à l'emploi à l'adresse NazaraEngine-master/examples/dynaterrain/MyHeightSource2D.hpp et MyHeightSource2D.cpp. Il vous suffit de rajouter ces deux fichiers à votre projet comme n'importe quelle autre classe perso.

Bonne nouvelle, votre projet est prêt à accueillir un terrain !

#include <Nazara/DynaTerrain/DynamicTerrain.hpp>
#include <Nazara/DynaTerrain/TerrainConfiguration.hpp>
#include "MyHeightSource2D.hpp"


On inclue les fichiers nécessaires au terrain.

int main()
{
    NzScene scene;

    // On instancie notre source de hauteur personnalisée (la classe récupérée dans examples/dynaterrain)
    MyHeightSource2D source2;
    //On créé notre configuration, on n'y touche pas pour l'instant, on utilise celle par défaut
    NzTerrainConfiguration myConfig;
    //On créé le terrain, on lui affecte la source de hauteur et sa configuration
    NzDynamicTerrain terrain(myConfig,&source2);
    //Et on l'initialise
    terrain.Initialize();
    //Et on l'attache à la scène
    terrain.SetParent(scene);


Et dans la boucle principale, afin que le terrain soit mis à jour en temps réel :
terrain.Update(camera.GetTranslation());
Le rendu est quand à lui provoqué par la scène, vous n'avez pas à vous en soucier.

Le code complet est donc le suivant :
#include <Nazara/3D.hpp>
#include <Nazara/Renderer/Renderer.hpp>
#include <Nazara/DynaTerrain/DynamicTerrain.hpp>
#include <Nazara/DynaTerrain/TerrainConfiguration.hpp>
#include "MyHeightSource2D.hpp"
#include <iostream>

using namespace std;

int main()
{
NzContextParameters::defaultCompatibilityProfile = true;
NzInitializer<Nz3D> nazara;
if (!nazara)
{
std::cout << "Failed to initialize Nazara, see NazaraLog.log for further informations" << std::endl;
std::getchar();
return EXIT_FAILURE;
}
    NzScene scene

    MyHeightSource2D source2;
    NzTerrainConfiguration myConfig;
    NzDynamicTerrain terrain(myConfig,&source2);
    terrain.Initialize();
    terrain.SetParent(scene);


    ///Code classique pour ouvrir une fenêtre avec Nazara
    NzString windowTitle("DynaTerrain example");
    NzRenderWindow window(NzVideoMode(800,600,32),windowTitle,nzWindowStyle_Default);
    window.SetFramerateLimit(100);
    NzRenderer::SetMatrix(nzMatrixType_Projection, NzMatrix4f::Perspective(NzDegrees(70.f), static_cast<float> (window.GetWidth())/window.GetHeight(), 1.f, 100000.f));

    NzClock secondClock, updateClock; // Des horloges pour gérer le temps
    unsigned int fps = 0; // Compteur de FPS
    NzMatrix4f matrix;
    matrix.MakeIdentity();
    NzRenderer::SetMatrix(nzMatrixType_View, NzMatrix4f::LookAt(NzVector3f(0.f,0.f,0.f), NzVector3f::Forward()));

    // Notre caméra
    NzVector3f camPos(-2000.f, 1800.f, 2000.f);
    NzEulerAnglesf camRot(-30.f, -45.f, 0.f);

    NzNode camera;
    camera.SetTranslation(camPos);
    camera.SetRotation(camRot);

    float camSpeed = 80.f;
    float sensitivity = 0.2f;

    // Quelques variables
    bool camMode = true;
    window.SetCursor(nzWindowCursor_None);
    bool windowOpen = true;
    bool drawWireframe = false;
    bool terrainUpdate = true;

    while (windowOpen)
    {
NzEvent event;
while (window.PollEvent(&event))
        {
switch (event.type)
{
case nzEventType_Quit:
windowOpen = false;
break;

case nzEventType_MouseMoved:
{
// Si nous ne sommes pas en mode free-fly, on ne traite pas l'évènement
if (!camMode)
break;

camRot.yaw = NzNormalizeAngle(camRot.yaw - event.mouseMove.deltaX*sensitivity);
camRot.pitch = NzClamp(camRot.pitch - event.mouseMove.deltaY*sensitivity, -89.f, 89.f);

camera.SetRotation(camRot);
NzMouse::SetPosition(window.GetWidth()/2, window.GetHeight()/2, window);
break;
}

case nzEventType_MouseButtonPressed:
if (event.mouseButton.button == NzMouse::Left)
{
if (camMode)
{
camMode = false;
window.SetCursor(nzWindowCursor_Default);
}
else
{
camMode = true;
window.SetCursor(nzWindowCursor_None);
}
}
                                break;
case nzEventType_Resized:
NzRenderer::SetViewport(NzRectui(0, 0, event.size.width, event.size.height));
NzRenderer::SetMatrix(nzMatrixType_Projection, NzMatrix4f::Perspective(NzDegrees(70.f), static_cast<float>(event.size.width)/event.size.height, 1.f, 10000.f));
break;

        case nzEventType_KeyPressed:
{
switch (event.key.code)
{
case NzKeyboard::Escape:
windowOpen = false;
break;

case NzKeyboard::F1:
if (drawWireframe)
{
drawWireframe = false;
NzRenderer::SetFaceFilling(nzFaceFilling_Fill);
}
else
{
drawWireframe = true;
NzRenderer::SetFaceFilling(nzFaceFilling_Line);
}
break;

                        case NzKeyboard::F2:
                                 terrainUpdate = !terrainUpdate;
                                 break;

default:
        break;
}

break;
}

default:
break;
}
}

// Mise à jour de la partie logique
if (updateClock.GetMilliseconds() >= 1000/60) // 60 fois par seconde
{
float elapsedTime = updateClock.GetSeconds();

static const NzVector3f forward(NzVector3f::Forward());
static const NzVector3f left(NzVector3f::Left());
static const NzVector3f up(NzVector3f::Up());

float speed2 = (NzKeyboard::IsKeyPressed(NzKeyboard::Key::LShift)) ? camSpeed*7: camSpeed;
                NzVector3f speed(speed2,speed2,speed2);

if (NzKeyboard::IsKeyPressed(NzKeyboard::Z))
camera.Translate(forward * speed * elapsedTime);

if (NzKeyboard::IsKeyPressed(NzKeyboard::S))
camera.Translate(-forward * speed * elapsedTime);

if (NzKeyboard::IsKeyPressed(NzKeyboard::Q))
camera.Translate(left * speed * elapsedTime);

if (NzKeyboard::IsKeyPressed(NzKeyboard::D))
camera.Translate(-left * speed * elapsedTime);

// En revanche, ici la hauteur est toujours la même, peu importe notre orientation
if (NzKeyboard::IsKeyPressed(NzKeyboard::Space))
camera.Translate(up * speed * elapsedTime, nzCoordSys_Global);

if (NzKeyboard::IsKeyPressed(NzKeyboard::LControl))
camera.Translate(up * speed * elapsedTime, nzCoordSys_Global);

updateClock.Restart();
}

        camera.Activate();
scene.Update();
scene.Cull();
scene.UpdateVisible();

        //On met à jour le terrain
        if(terrainUpdate)
        {
            terrain.Update(camera.GetTranslation());
        }

        // Nous mettons à jour l'écran
window.Display();

fps++;

// Toutes les secondes
if (secondClock.GetMilliseconds() >= 1000)
{
window.SetTitle(windowTitle + " (FPS: " + NzString::Number(fps) + ')' + "( Camera in : " + camera.GetTranslation() + ")");
fps = 0;
secondClock.Restart();
}
   }

    return 0;
}
« Modifié: avril 27, 2013, 07:25:07 pm par Overdrivr »

Hors ligne Overdrivr

  • Modérateur
  • Nouveau
  • *****
  • Messages: 3
    • Voir le profil
Re : Nazara Terrain - Tutoriel / Fonctionnement
« Réponse #1 le: mars 19, 2013, 03:52:21 pm »
Ce post sera dédié au fonctionnement interne du module, si vous êtes un simple utilisateur, il n'est pas indispensable de le lire mais ça peut clarifier des choses.

Quadtree

Gérer un maillage dynamique n'est pas spécialement compliqué, mais requiert un peu d'organisation. Comme certaines zones du terrain seront plus denses en triangles que d'autres, il faut un moyen de conserver en mémoire l'état du maillage, et de le modifier correctement lorsque c'est nécessaire.

Ici, la structure de données qui se prête bien au problème est un arbre de données, et plus précisément un arbre dont chaque noeud a systématiquement 4 enfants, c'est à dire un quadtree.

Par rapport à un simple tableau à deux dimensions, le quadtree possède plusieurs avantages :
  • Mieux profiter de la représentation spatiale (un node enfant est spatialement contenu dans son parent)
  • Implémentation très efficace de techniques de culling, et plus généralement d'algorithmes hiérarchiques
  • Représentation facile des données sur plusieurs niveaux différents

La logique derrière le terrain utilise donc un quadtree. Ici, le quadtree est composé d'une classe de centralisation (NzTerrainQuadTree) et d'un ensemble de noeuds (NzTerrainInternalNode).

Les noeuds les plus en bas de l'arbre, ceux qui n'ont pas d'enfants, sont ceux qui doivent communiquer avec le GPU afin que leur maillage soit dessiné.

Maillage atomique
Le maillage atomique représente le plus petit maillage constituant le terrain, un terrain entier peut être constitué de milliers de ce maillage atomique. Mon choix ici a été de choisir le maillage atomique suivant :



C'est un simple maillage carré constitué de 64 triangles. C'est un choix purement arbitraire, je tiens à le souligner. Parce que la gestion de ce maillage n'est pas triviale, la classe NzPatch est en charge de ces opérations

NzPatch
Chaque NzInternalNode possède un pointeur vers un NzPatch. Lorsqu'un NzInternalNode est au plus bas de l'arbre (on l'appelle aussi un node feuille), ce pointeur est initialisé de manière à être valide. Cette action entraîne à son tour l'initialisation d'un NzPatch, qui effectue diverses opérations sur le maillage atomique (récupération de la hauteur de chaque point, calcul des normales, etc.). Ce maillage est transmis à une classe spécialisée, et en bout de chaîne, ce maillage est uploadé sur le GPU, et affiché.

NzInternalNode ne gère donc absolument pas de maillage, il se contente d'initialiser un NzPatch au bon moment.

Logique vs Affichage
Autant que possible, j'ai essayé de découpler la partie logique de l'affichage, ce qui explique ce qui va suivre.

Contrairement à ce que vous pouvez penser, le quadtree dont je viens de vous parler n'est pas celui que l'on va utiliser pour effectuer les appels au rendu, le culling, etc. Pourquoi ?

Rappelez vous que les maillages atomiques, une fois préparés, ont besoin d'être chargés dans un buffer et uploadés sur le GPU. Avec une utilisation directe de ce quadtree, on n'a pas le choix, il faut un buffer par node, ce buffer contenant donc un seul maillage atomique.

Si notre arbre est un minimum profond par endroits, on atteint facilement 50000 nodes au total, donc 50000 buffers, et à chaque frame une boucle de 50000 nodes à parcourir. Sur nos processeurs modernes, c'est une rigolade, mais ce chiffre augmente exponentiellement lorsque la profondeur de l'arbre augmente. De plus, l'allocation et la désallocation de mémoire sur le GPU prend du temps, si il faut allouer quelques centaines de buffers GPU par seconde les performances vont en prendre un coup.

J'ai donc choisi d'utiliser une autre technique. Chaque NzPatch, après effectué ses tâches, transmet par référence constante son maillage à la classe NzTerrainMasterNode. Je ne détaillerais pas son fonctionnement interne dans ce post, mais sachez que cette fois-ci les maillages sont regroupés par lots, dans des buffers dont la taille est choisie manuellement à l'initialisation.

Au lieu de 50000, on passe à 25 buffers, et surtout le fait d'augmenter la précision du terrain ne demandera d'allouer que quelques buffers supplémentaires...

Le prochain post sera dédié à l'interfaçage avec le manager de scène.
« Modifié: mars 19, 2013, 03:58:01 pm par Overdrivr »