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 :
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 :
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 :
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 :
[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 :
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 :
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.



