Unity - Génération procédurale de terrain

Changement du niveau de détail

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Cette série explique comment générer de manière procédurale des continents dans Unity.
Vous pouvez retrouver les autres épisodes de cette série dans le sommaire dédié.

II. Vidéo


Unity - Génération procédurale de terrain - Changement du niveau de détail


III. Résumé

Dans cet épisode, nous allons faire en sorte que les sections de terrain les plus proches du joueur aient plus de détails alors que les sections au loin soient moins détaillées. De plus, la résolution du terrain changera dynamiquement suivant la distance avec le joueur.

IV. Implémentation

Pour commencer l'implémentation du dynamisme dans le niveau de détail, nous ajoutons une classe LODMesh dans le script EndlessTerrain.cs :

EndlessTerrain.cs
Sélectionnez
class LODMesh {

        public Mesh mesh;
        public bool hasRequestedMesh;
        public bool hasMesh;
        int lod;

        public LODMesh(int lod) {
            this.lod = lod;
        }

        void OnMeshDataReceived(MeshData meshData) {
            mesh = meshData.CreateMesh ();
            hasMesh = true;
        }

        public void RequestMesh(MapData mapData) {
            hasRequestedMesh = true;
            mapGenerator.RequestMeshData (mapData, lod, OnMeshDataReceived);
        }

    }

Celle-ci permet de charger à la volée des modèles d'un niveau de détail différent. Sachant qu'à partir de maintenant nous spécifions le niveau de détail lors de la génération du modèle, nous devons mettre à jour les méthodes MapGenerator::RequestMeshData et MapGenerator::MeshDataThread.

En complément, on ajoute une structure LODInfo :

EndlessTerrain.cs
Sélectionnez
[System.Serializable]
    public struct LODInfo {
        public int lod;
        public float visibleDstThreshold;
}

Qui permet de configurer à partir de quel moment tel ou tel niveau de détail est nécessaire. La variable visible dans l'éditeur est un tableau comme suit :

EndlessTerrain.cs
Sélectionnez
public LODInfo[] detailLevels;

Aussi, la distance maximale pour laquelle une section de terrain est visible est calculée à la volée par rapport au dernier élément du tableau.

Ensuite, la classe TerrainChunk possède maintenant un tableau de modèles pour chaque niveau de détail.
La méthode UpdateTerrainChunk() est mise à jour pour déterminer quel niveau de détail est à afficher :

EndlessTerrain.cs
Sélectionnez
public void UpdateTerrainChunk() {
    if (mapDataReceived) {
        float viewerDstFromNearestEdge = Mathf.Sqrt (bounds.SqrDistance (viewerPosition));
        bool visible = viewerDstFromNearestEdge <= maxViewDst;

        if (visible) {
            int lodIndex = 0;

            for (int i = 0; i < detailLevels.Length - 1; i++) {
                if (viewerDstFromNearestEdge > detailLevels [i].visibleDstThreshold) {
                    lodIndex = i + 1;
                } else {
                    break;
                }
            }

            if (lodIndex != previousLODIndex) {
                LODMesh lodMesh = lodMeshes [lodIndex];
                if (lodMesh.hasMesh) {
                    previousLODIndex = lodIndex;
                    meshFilter.mesh = lodMesh.mesh;
                } else if (!lodMesh.hasRequestedMesh) {
                    lodMesh.RequestMesh (mapData);
                }
            }
        }

        SetVisible (visible);
    }
}

La boucle permet de déterminer le niveau de détail suivant la distance des niveaux de détail configurés dans l'éditeur. Si le niveau de détail trouvé est le même que le précédent, alors on évite de rechercher le modèle. Aussi, si le modèle a déjà été demandé (mais n'est pas encore généré) alors on ne fait rien. Ainsi, on s'assure que la requête pour un modèle donné n'est effectuée qu'une seule fois.

Afin de mieux voir le résultat, vous pouvez passer le rendu en mode fil de fer (« wireframe »).

V. Optimisation

Dans cette première version, la distance entre le joueur et les sections de terrain est vérifiée (et donc possiblement mise à jour) à chaque image. Il est possible d'ajouter un seuil de déplacement du joueur, qui, une fois atteint, déclenche la mise à jour des niveaux de détail :

EndlessTerrain.cs
Sélectionnez
const float viewerMoveThresholdForChunkUpdate = 25f;
const float sqrViewerMoveThresholdForChunkUpdate = viewerMoveThresholdForChunkUpdate * viewerMoveThresholdForChunkUpdate;

Vector2 viewerPositionOld;

Et dans la méthode Update() :

EndlessTerrain.cs
Sélectionnez
void Update() {
    viewerPosition = new Vector2 (viewer.position.x, viewer.position.z);

    if ((viewerPositionOld - viewerPosition).sqrMagnitude > sqrViewerMoveThresholdForChunkUpdate) {
        viewerPositionOld = viewerPosition;
        UpdateVisibleChunks ();
    }
}

Il est plus efficace d'effectuer les comparaisons de distances avec le carré de celles-ci, afin d'éviter les coûteuses racines carrées.

Par contre, il faut bien appeler la fonction Update() lors de la réception des données, dans la classe LODMesh :

EndlessTerrain.cs
Sélectionnez
System.Action updateCallback;

public LODMesh(int lod, System.Action updateCallback) {
    this.lod = lod;
    this.updateCallback = updateCallback;
}

void OnMeshDataReceived(MeshData meshData) {
    mesh = meshData.CreateMesh ();
    hasMesh = true;

    updateCallback ();
}

Et dans la classe TerrainChunk :

EndlessTerrain.cs
Sélectionnez
void OnMapDataReceived(MapData mapData) {
    this.mapData = mapData;
    mapDataReceived = true;

    UpdateTerrainChunk ();
}

VI. Textures

Il ne reste plus qu'à appliquer les textures :

EndlessTerrain.cs
Sélectionnez
void OnMapDataReceived(MapData mapData) {
    this.mapData = mapData;
    mapDataReceived = true;

    Texture2D texture = TextureGenerator.TextureFromColourMap (mapData.colourMap, MapGenerator.mapChunkSize, MapGenerator.mapChunkSize);
    meshRenderer.material.mainTexture = texture;

    UpdateTerrainChunk ();
}

VII. Répétitions

Comme vous avez pu le remarquer, chaque morceau de terrain est une copie du précédent. Cela peut être corrigé en passant un centre en paramètre de la génération de terrain, soit, à la méthode MapGenerator::GenerateMapData() :

MapGenerator.cs
Sélectionnez
MapData GenerateMapData(Vector2 centre) {
    float[,] noiseMap = Noise.GenerateNoiseMap (mapChunkSize, mapChunkSize, seed, noiseScale, octaves, persistance, lacunarity, centre + offset);

    Color[] colourMap = new Color[mapChunkSize * mapChunkSize];
    for (int y = 0; y < mapChunkSize; y++) {
        for (int x = 0; x < mapChunkSize; x++) {
            float currentHeight = noiseMap [x, y];
            for (int i = 0; i < regions.Length; i++) {
                if (currentHeight <= regions [i].height) {
                    colourMap [y * mapChunkSize + x] = regions [i].colour;
                    break;
                }
            }
        }
    }


    return new MapData (noiseMap, colourMap);
}

VIII. Commenter

Vous pouvez commenter et donner vos avis dans la discussion associée sur le forum.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2017 Unity Technologies. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.