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

Génération des normales

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 - Normales


III. Résumé

III-A. Les normales

Les normales sont des vecteurs perpendiculaires aux triangles constituant un modèle 3D. Ainsi, ils représentent l'orientation de la face. Les normales permettent de calculer la lumière reçue par les faces d'un objet.
Il existe deux façons de définir les normales d'un objet :

  • sur une base d'une normale par face ;
  • sur une base d'une normale par objet.

La seconde option donne un meilleur résultat graphique. La normale au sommet est calculée en prenant la normale de chaque face partageant le sommet.

III-B. Les bordures de terrain

Si vous utilisez le mode de normalisation (« Normalize Mode ») à la valeur « Global » et que vous zoomez sur une bordure entre deux morceaux de terrains, vous verrez des défauts. En effet, les normales à la limite de chaque morceau ne sont pas partagées (et donc non prises en compte) avec le morceau suivant, de sorte que pour un même sommet elles diffèrent. Cela provoque donc un éclairage non cohérent pour les triangles en bordure des morceaux de terrain.

III-C. Correction du problème

Pour corriger ce problème, nous allons créer notre propre implémentation de la fonction RecalculateNormals() :

 
Sélectionnez
Vector3[] CalculateNormals() {

    Vector3[] vertexNormals = new Vector3[vertices.Length];
    int triangleCount = triangles.Length / 3;
    for (int i = 0; i < triangleCount; i++) {
        int normalTriangleIndex = i * 3;
        int vertexIndexA = triangles [normalTriangleIndex];
        int vertexIndexB = triangles [normalTriangleIndex + 1];
        int vertexIndexC = triangles [normalTriangleIndex + 2];

        Vector3 triangleNormal = SurfaceNormalFromIndices (vertexIndexA, vertexIndexB, vertexIndexC);
        vertexNormals [vertexIndexA] += triangleNormal;
        vertexNormals [vertexIndexB] += triangleNormal;
        vertexNormals [vertexIndexC] += triangleNormal;
    }

    for (int i = 0; i < vertexNormals.Length; i++) {
        vertexNormals [i].Normalize ();
    }

    return vertexNormals;

}

La fonction calcule les normales pour chaque triangle. Si un triangle partage le même sommet, alors la normale est additionnée à la précédente.

Pour simplifier le code, une seconde fonction pour calculer la normale à partir de trois index est implémentée :

 
Sélectionnez
    Vector3 SurfaceNormalFromIndices(int indexA, int indexB, int indexC) {
        Vector3 pointA = vertices [indexA];
        Vector3 pointB = vertices [indexB];
        Vector3 pointC = vertices [indexC];

        Vector3 sideAB = pointB - pointA;
        Vector3 sideAC = pointC - pointA;
        return Vector3.Cross (sideAB, sideAC).normalized;
}

La normale d'un triangle peut se calculer grâce au produit vectoriel de deux vecteurs représentant les côtés de ce triangle.

III-C-1. Gestion des bordures

Au cours de cet épisode, l'auteur simplifie la gestion de la longueur et de la largeur du modèle. En effet, il génère que des modèles carrés.

Le calcul des normales doit prendre en compte les triangles en bordure des morceaux de terrain adjacents (et ce, même s'ils ne sont pas affichés). Pour mettre cela en place, une notion de taille avec bordure (« bordered size ») et de taille de modèle (« mesh size ») est implémentée :

MeshGenerator.cs
Sélectionnez
public static MeshData GenerateTerrainMesh(float[,] heightMap, float heightMultiplier, AnimationCurve _heightCurve, int levelOfDetail) {
    AnimationCurve heightCurve = new AnimationCurve (_heightCurve.keys);

    int meshSimplificationIncrement = (levelOfDetail == 0)?1:levelOfDetail * 2;

    int borderedSize = heightMap.GetLength (0);
    int meshSize = borderedSize - 2*meshSimplificationIncrement;
    int meshSizeUnsimplified = borderedSize - 2;

    float topLeftX = (meshSizeUnsimplified - 1) / -2f;
    float topLeftZ = (meshSizeUnsimplified - 1) / 2f;


    int verticesPerLine = (meshSize - 1) / meshSimplificationIncrement + 1;

    MeshData meshData = new MeshData (verticesPerLine);

    int[,] vertexIndicesMap = new int[borderedSize,borderedSize];
    int meshVertexIndex = 0;
    int borderVertexIndex = -1;

    for (int y = 0; y < borderedSize; y += meshSimplificationIncrement) {
        for (int x = 0; x < borderedSize; x += meshSimplificationIncrement) {
            bool isBorderVertex = y == 0 || y == borderedSize - 1 || x == 0 || x == borderedSize - 1;

            if (isBorderVertex) {
                vertexIndicesMap [x, y] = borderVertexIndex;
                borderVertexIndex--;
            } else {
                vertexIndicesMap [x, y] = meshVertexIndex;
                meshVertexIndex++;
            }
        }
    }

    for (int y = 0; y < borderedSize; y += meshSimplificationIncrement) {
        for (int x = 0; x < borderedSize; x += meshSimplificationIncrement) {
            int vertexIndex = vertexIndicesMap [x, y];
            Vector2 percent = new Vector2 ((x-meshSimplificationIncrement) / (float)meshSize, (y-meshSimplificationIncrement) / (float)meshSize);
            float height = heightCurve.Evaluate (heightMap [x, y]) * heightMultiplier;
            Vector3 vertexPosition = new Vector3 (topLeftX + percent.x * meshSizeUnsimplified, height, topLeftZ - percent.y * meshSizeUnsimplified);

            meshData.AddVertex (vertexPosition, percent, vertexIndex);

            if (x < borderedSize - 1 && y < borderedSize - 1) {
                int a = vertexIndicesMap [x, y];
                int b = vertexIndicesMap [x + meshSimplificationIncrement, y];
                int c = vertexIndicesMap [x, y + meshSimplificationIncrement];
                int d = vertexIndicesMap [x + meshSimplificationIncrement, y + meshSimplificationIncrement];
                meshData.AddTriangle (a,d,c);
                meshData.AddTriangle (d,a,b);
            }

            vertexIndex++;
        }
    }

    return meshData;

}

Maintenant, un tableau d'indices est généré. Dans celui-ci, les indices représentant la bordure seront négatifs, et les indices qui doivent être affichés par la suite restent les mêmes (positifs).

Il est nécessaire d'être prudent avec le niveau de détail, sans quoi le modèle pourrait avoir un décalage avec les morceaux adjacents.

Il est fortement conseillé de travailler avec une feuille de papier et un crayon pour se représenter concrètement les opérations à effectuer.

Aussi, la classe MeshData est modifiée :

MeshGenerator.cs
Sélectionnez
public class MeshData {
    Vector3[] vertices;
    int[] triangles;
    Vector2[] uvs;

    Vector3[] borderVertices;
    int[] borderTriangles;

    int triangleIndex;
    int borderTriangleIndex;

    public MeshData(int verticesPerLine) {
        vertices = new Vector3[verticesPerLine * verticesPerLine];
        uvs = new Vector2[verticesPerLine * verticesPerLine];
        triangles = new int[(verticesPerLine-1)*(verticesPerLine-1)*6];

        borderVertices = new Vector3[verticesPerLine * 4 + 4];
        borderTriangles = new int[24 * verticesPerLine];
    }

    public void AddVertex(Vector3 vertexPosition, Vector2 uv, int vertexIndex) {
        if (vertexIndex < 0) {
            borderVertices [-vertexIndex - 1] = vertexPosition;
        } else {
            vertices [vertexIndex] = vertexPosition;
            uvs [vertexIndex] = uv;
        }
    }

    public void AddTriangle(int a, int b, int c) {
        if (a < 0 || b < 0 || c < 0) {
            borderTriangles [borderTriangleIndex] = a;
            borderTriangles [borderTriangleIndex + 1] = b;
            borderTriangles [borderTriangleIndex + 2] = c;
            borderTriangleIndex += 3;
        } else {
            triangles [triangleIndex] = a;
            triangles [triangleIndex + 1] = b;
            triangles [triangleIndex + 2] = c;
            triangleIndex += 3;
        }
    }
    // ...

}

Ainsi, la classe contient maintenant les informations de la bordure. De cette façon, les variables membres ne sont plus publiques et il est donc nécessaire d'ajouter une fonction AddVertex() pour permettre d'ajouter des sommets aux tableaux. Suivant l'index (s'il est négatif ou non), le sommet sera ajouté au tableau des sommets de bordure ou au tableau des sommets du modèle.
De même pour la fonction AddTriangle(), il est nécessaire de vérifier les index passés à la fonction pour savoir quel tableau recevra le triangle.

Finalement, la fonction CalculateNormals() est mise à jour pour prendre en compte les triangles de bordure :

 
Sélectionnez
Vector3[] CalculateNormals() {

    Vector3[] vertexNormals = new Vector3[vertices.Length];
    int triangleCount = triangles.Length / 3;
    for (int i = 0; i < triangleCount; i++) {
        int normalTriangleIndex = i * 3;
        int vertexIndexA = triangles [normalTriangleIndex];
        int vertexIndexB = triangles [normalTriangleIndex + 1];
        int vertexIndexC = triangles [normalTriangleIndex + 2];

        Vector3 triangleNormal = SurfaceNormalFromIndices (vertexIndexA, vertexIndexB, vertexIndexC);
        vertexNormals [vertexIndexA] += triangleNormal;
        vertexNormals [vertexIndexB] += triangleNormal;
        vertexNormals [vertexIndexC] += triangleNormal;
    }

    int borderTriangleCount = borderTriangles.Length / 3;
    for (int i = 0; i < borderTriangleCount; i++) {
        int normalTriangleIndex = i * 3;
        int vertexIndexA = borderTriangles [normalTriangleIndex];
        int vertexIndexB = borderTriangles [normalTriangleIndex + 1];
        int vertexIndexC = borderTriangles [normalTriangleIndex + 2];

        Vector3 triangleNormal = SurfaceNormalFromIndices (vertexIndexA, vertexIndexB, vertexIndexC);
        if (vertexIndexA >= 0) {
            vertexNormals [vertexIndexA] += triangleNormal;
        }
        if (vertexIndexB >= 0) {
            vertexNormals [vertexIndexB] += triangleNormal;
        }
        if (vertexIndexC >= 0) {
            vertexNormals [vertexIndexC] += triangleNormal;
        }
    }


    for (int i = 0; i < vertexNormals.Length; i++) {
        vertexNormals [i].Normalize ();
    }

    return vertexNormals;

}

La fonction SurfaceNormalFromIndices() doit aussi gérer le nouveau cas d'indice négatifs :

 
Sélectionnez
    Vector3 SurfaceNormalFromIndices(int indexA, int indexB, int indexC) {
        Vector3 pointA = (indexA < 0)?borderVertices[-indexA-1] : vertices [indexA];
        Vector3 pointB = (indexB < 0)?borderVertices[-indexB-1] : vertices [indexB];
        Vector3 pointC = (indexC < 0)?borderVertices[-indexC-1] : vertices [indexC];

        Vector3 sideAB = pointB - pointA;
        Vector3 sideAC = pointC - pointA;
        return Vector3.Cross (sideAB, sideAC).normalized;
    }

Enfin, la génération des données des morceaux de terrain doit être modifiée afin de générer des cartes légèrement plus grandes :

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

Cette modification demande aussi un changement de la variable mapChunkSize :

MapGenerator.cs
Sélectionnez
public const int mapChunkSize = 239;

afin de rester divisible par l'incrément lors de la simplification du modèle.

IV. 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 © 2018 Unity Technologies. Aucune reproduction, même partielle, ne peut être faite de ce site et 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.