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.