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 :
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 :
[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 :
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 :
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 :
const float viewerMoveThresholdForChunkUpdate = 25f;
const float sqrViewerMoveThresholdForChunkUpdate = viewerMoveThresholdForChunkUpdate * viewerMoveThresholdForChunkUpdate;
Vector2 viewerPositionOld;Et dans la méthode Update() :
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 :
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 :
void OnMapDataReceived(MapData mapData) {
this.mapData = mapData;
mapDataReceived = true;
UpdateTerrainChunk ();
}VI. Textures▲
Il ne reste plus qu'à appliquer les textures :
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() :
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.



