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.