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

Refactorisation

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


III. Résumé

Le but de cet épisode est d'améliorer le code. En effet, au fil des évolutions et des améliorations, le code est devenu brouillon.

III-A. Classe MapGenerator

La classe MapGenerator fait actuellement trois choses différentes. Il est bon de rappeler que théoriquement, une classe ne devrait s'occuper que d'une seule tâche. Actuellement, elle permet de :

  • prévisualiser le terrain dans l'éditeur ;
  • gérer les requêtes de données du terrain ;
  • de générer les données du terrain.

La classe sera donc découpée en trois.

Aussi en regardant plus en détail le contenu du fichier MapGenerator.cs, on remarque que la classe MapData, contenant ce qui semble être les hauteurs du terrain, ne contient pas réellement les hauteurs, car les valeurs contenues vont être transformées par la suite.

III-B. Hauteurs du terrain

La classe MapData est renommée HeightMap. La variable membre est renommée values. Deux nouvelles variables membres, en lecture seule, sont ajoutées :

MapGenerator.cs
Sélectionnez
public struct HeightMap {
    public readonly float[,] values;
    public readonly float minValue;
    public readonly float maxValue;

    public HeightMap (float[,] values, float minValue, float maxValue)
    {
        this.values = values;
        this.minValue = minValue;
        this.maxValue = maxValue;
    }
}

Finalement, la classe est déplacée dans un nouveau fichier HeightMapGenerator.cs.

III-C. Génération des données

La classe HeightMapGenerator (implémentée dans le fichier HeightMapGenerator.cs) fournit une fonction GenerateHeightMap :

HeightMapGenerator.cs
Sélectionnez
public static HeightMap GenerateHeightMap(int width, int height, HeightMapSettings settings, Vector2 sampleCentre) {
    float[,] values = Noise.GenerateNoiseMap (width, height, settings.noiseSettings, sampleCentre);

    AnimationCurve heightCurve_threadsafe = new AnimationCurve (settings.heightCurve.keys);

    float minValue = float.MaxValue;
    float maxValue = float.MinValue;

    for (int i = 0; i < width; i++) {
        for (int j = 0; j < height; j++) {
            values [i, j] *= heightCurve_threadsafe.Evaluate (values [i, j]) * settings.heightMultiplier;

            if (values [i, j] > maxValue) {
                maxValue = values [i, j];
            }
            if (values [i, j] < minValue) {
                minValue = values [i, j];
            }
        }
    }

    return new HeightMap (values, minValue, maxValue);
}

Le type HeightMapSettings est implémenté dans le fichier HeightMapSettings.cs et remplace la classe NoiseData :

HeightMapSettings.cs
Sélectionnez
using UnityEngine;
using System.Collections;

[CreateAssetMenu()]
public class HeightMapSettings : UpdatableData {

    public NoiseSettings noiseSettings;

    public bool useFalloff;

    public float heightMultiplier;
    public AnimationCurve heightCurve;

    public float minHeight {
        get {
            return heightMultiplier * heightCurve.Evaluate (0);
        }
    }

    public float maxHeight {
        get {
            return heightMultiplier * heightCurve.Evaluate (1);
        }
    }

    #if UNITY_EDITOR

    protected override void OnValidate() {
        noiseSettings.ValidateValues ();
        base.OnValidate ();
    }
    #endif

}

Les données spécifiques au bruit sont implémentées dans le fichier Noise.cs :

Noise.cs
Sélectionnez
[System.Serializable]
public class NoiseSettings {
    public Noise.NormalizeMode normalizeMode;

    public float scale = 50;

    public int octaves = 6;
    [Range(0,1)]
    public float persistance =.6f;
    public float lacunarity = 2;

    public int seed;
    public Vector2 offset;

    public void ValidateValues() {
        scale = Mathf.Max (scale, 0.01f);
        octaves = Mathf.Max (octaves, 1);
        lacunarity = Mathf.Max (lacunarity, 1);
        persistance = Mathf.Clamp01 (persistance);
    }
}

Maintenant que nous avons une classe contenant tous les paramètres du bruit, nous pouvons l'utiliser pour transférer les paramètres à la fonction GenerateNoiseMap(). Aussi, un nouveau paramètre est utilisé pour transférer le centre de l'échantillonnage.
De plus, des améliorations sont apportées à la fonction. Voici le code résultant :

Noise.cs
Sélectionnez
public static float[,] GenerateNoiseMap(int mapWidth, int mapHeight, NoiseSettings settings, Vector2 sampleCentre) {
    float[,] noiseMap = new float[mapWidth,mapHeight];

    System.Random prng = new System.Random (settings.seed);
    Vector2[] octaveOffsets = new Vector2[settings.octaves];

    float maxPossibleHeight = 0;
    float amplitude = 1;
    float frequency = 1;

    for (int i = 0; i < settings.octaves; i++) {
        float offsetX = prng.Next (-100000, 100000) + settings.offset.x + sampleCentre.x;
        float offsetY = prng.Next (-100000, 100000) - settings.offset.y - sampleCentre.y;
        octaveOffsets [i] = new Vector2 (offsetX, offsetY);

        maxPossibleHeight += amplitude;
        amplitude *= settings.persistance;
    }

    float maxLocalNoiseHeight = float.MinValue;
    float minLocalNoiseHeight = float.MaxValue;

    float halfWidth = mapWidth / 2f;
    float halfHeight = mapHeight / 2f;


    for (int y = 0; y < mapHeight; y++) {
        for (int x = 0; x < mapWidth; x++) {

            amplitude = 1;
            frequency = 1;
            float noiseHeight = 0;

            for (int i = 0; i < settings.octaves; i++) {
                float sampleX = (x-halfWidth + octaveOffsets[i].x) / settings.scale * frequency;
                float sampleY = (y-halfHeight + octaveOffsets[i].y) / settings.scale * frequency;

                float perlinValue = Mathf.PerlinNoise (sampleX, sampleY) * 2 - 1;
                noiseHeight += perlinValue * amplitude;

                amplitude *= settings.persistance;
                frequency *= settings.lacunarity;
            }

            if (noiseHeight > maxLocalNoiseHeight) {
                maxLocalNoiseHeight = noiseHeight;
            } 
            if (noiseHeight < minLocalNoiseHeight) {
                minLocalNoiseHeight = noiseHeight;
            }
            noiseMap [x, y] = noiseHeight;

            if (settings.normalizeMode == NormalizeMode.Global) {
                float normalizedHeight = (noiseMap [x, y] + 1) / (maxPossibleHeight / 0.9f);
                noiseMap [x, y] = Mathf.Clamp (normalizedHeight, 0, int.MaxValue);
            }
        }
    }

    if (settings.normalizeMode == NormalizeMode.Local) {
        for (int y = 0; y < mapHeight; y++) {
            for (int x = 0; x < mapWidth; x++) {
                noiseMap [x, y] = Mathf.InverseLerp (minLocalNoiseHeight, maxLocalNoiseHeight, noiseMap [x, y]);
            }
        }
}

    return noiseMap;
}

Sachant que la majorité des données anciennement contenues dans TextureData sont dans la classe HeightMapSettings, la classe TextureData n'a plus vraiment lieu d'être. Renommons-la MeshSettings et complétons-la avec les données adéquates :

MeshSettings.cs
Sélectionnez
using UnityEngine;
using System.Collections;

[CreateAssetMenu()]
public class MeshSettings : UpdatableData {

    public const int numSupportedLODs = 5;
    public const int numSupportedChunkSizes = 9;
    public const int numSupportedFlatshadedChunkSizes = 3;
    public static readonly int[] supportedChunkSizes = {48,72,96,120,144,168,192,216,240};
    
    public float meshScale = 2.5f;
    public bool useFlatShading;

    [Range(0,numSupportedChunkSizes-1)]
    public int chunkSizeIndex;
    [Range(0,numSupportedFlatshadedChunkSizes-1)]
    public int flatshadedChunkSizeIndex;


    // num verts per line of mesh rendered at LOD = 0. Includes the 2 extra verts that are excluded from final mesh, but used for calculating normals
    public int numVertsPerLine {
        get {
            return supportedChunkSizes [(useFlatShading) ? flatshadedChunkSizeIndex : chunkSizeIndex] + 1;
        }
    }

    public float meshWorldSize {
        get {
            return (numVertsPerLine - 3) * meshScale;
        }
    }


}

Par la même occasion, la fonction mapChunkSize est renommée numVertsPerLine et est réécrite de manière plus concise.

La fonction GenerateTerrainMesh() est modifiée pour prendre en paramètre une variable de type MeshSettings.

De même, les classes MapGenerator et EndlessTerrain doivent être mises à jour pour correspondre aux modifications de ce chapitre.

IV. Code source

Vous pouvez consulter le code de ce chapitre sur le GitHub de Sebastian Lague.

V. 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.