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 - Normales
III. Résumé▲
III-A. Les normales▲
Les normales sont des vecteurs perpendiculaires aux triangles constituant un modèle 3D. Ainsi, ils représentent l'orientation de la face. Les normales permettent de calculer la lumière reçue par les faces d'un objet.
Il existe deux façons de définir les normales d'un objet :
- sur une base d'une normale par face ;
- sur une base d'une normale par objet.
La seconde option donne un meilleur résultat graphique. La normale au sommet est calculée en prenant la normale de chaque face partageant le sommet.
III-B. Les bordures de terrain▲
Si vous utilisez le mode de normalisation (« Normalize Mode ») à la valeur « Global » et que vous zoomez sur une bordure entre deux morceaux de terrains, vous verrez des défauts. En effet, les normales à la limite de chaque morceau ne sont pas partagées (et donc non prises en compte) avec le morceau suivant, de sorte que pour un même sommet elles diffèrent. Cela provoque donc un éclairage non cohérent pour les triangles en bordure des morceaux de terrain.
III-C. Correction du problème▲
Pour corriger ce problème, nous allons créer notre propre implémentation de la fonction RecalculateNormals() :
Vector3[]
CalculateNormals
(
) {
Vector3[]
vertexNormals =
new
Vector3[
vertices.
Length];
int
triangleCount =
triangles.
Length /
3
;
for
(
int
i =
0
;
i <
triangleCount;
i++
) {
int
normalTriangleIndex =
i *
3
;
int
vertexIndexA =
triangles [
normalTriangleIndex];
int
vertexIndexB =
triangles [
normalTriangleIndex +
1
];
int
vertexIndexC =
triangles [
normalTriangleIndex +
2
];
Vector3 triangleNormal =
SurfaceNormalFromIndices (
vertexIndexA,
vertexIndexB,
vertexIndexC);
vertexNormals [
vertexIndexA]
+=
triangleNormal;
vertexNormals [
vertexIndexB]
+=
triangleNormal;
vertexNormals [
vertexIndexC]
+=
triangleNormal;
}
for
(
int
i =
0
;
i <
vertexNormals.
Length;
i++
) {
vertexNormals [
i].
Normalize (
);
}
return
vertexNormals;
}
La fonction calcule les normales pour chaque triangle. Si un triangle partage le même sommet, alors la normale est additionnée à la précédente.
Pour simplifier le code, une seconde fonction pour calculer la normale à partir de trois index est implémentée :
Vector3 SurfaceNormalFromIndices
(
int
indexA,
int
indexB,
int
indexC) {
Vector3 pointA =
vertices [
indexA];
Vector3 pointB =
vertices [
indexB];
Vector3 pointC =
vertices [
indexC];
Vector3 sideAB =
pointB -
pointA;
Vector3 sideAC =
pointC -
pointA;
return
Vector3.
Cross (
sideAB,
sideAC).
normalized;
}
La normale d'un triangle peut se calculer grâce au produit vectoriel de deux vecteurs représentant les côtés de ce triangle.
III-C-1. Gestion des bordures▲
Au cours de cet épisode, l'auteur simplifie la gestion de la longueur et de la largeur du modèle. En effet, il génère que des modèles carrés.
Le calcul des normales doit prendre en compte les triangles en bordure des morceaux de terrain adjacents (et ce, même s'ils ne sont pas affichés). Pour mettre cela en place, une notion de taille avec bordure (« bordered size ») et de taille de modèle (« mesh size ») est implémentée :
public
static
MeshData GenerateTerrainMesh
(
float
[,]
heightMap,
float
heightMultiplier,
AnimationCurve _heightCurve,
int
levelOfDetail) {
AnimationCurve heightCurve =
new
AnimationCurve (
_heightCurve.
keys);
int
meshSimplificationIncrement =
(
levelOfDetail ==
0
)?
1
:
levelOfDetail *
2
;
int
borderedSize =
heightMap.
GetLength (
0
);
int
meshSize =
borderedSize -
2
*
meshSimplificationIncrement;
int
meshSizeUnsimplified =
borderedSize -
2
;
float
topLeftX =
(
meshSizeUnsimplified -
1
) /
-
2f
;
float
topLeftZ =
(
meshSizeUnsimplified -
1
) /
2f
;
int
verticesPerLine =
(
meshSize -
1
) /
meshSimplificationIncrement +
1
;
MeshData meshData =
new
MeshData (
verticesPerLine);
int
[,]
vertexIndicesMap =
new
int
[
borderedSize,
borderedSize];
int
meshVertexIndex =
0
;
int
borderVertexIndex =
-
1
;
for
(
int
y =
0
;
y <
borderedSize;
y +=
meshSimplificationIncrement) {
for
(
int
x =
0
;
x <
borderedSize;
x +=
meshSimplificationIncrement) {
bool
isBorderVertex =
y ==
0
||
y ==
borderedSize -
1
||
x ==
0
||
x ==
borderedSize -
1
;
if
(
isBorderVertex) {
vertexIndicesMap [
x,
y]
=
borderVertexIndex;
borderVertexIndex--;
}
else
{
vertexIndicesMap [
x,
y]
=
meshVertexIndex;
meshVertexIndex++;
}
}
}
for
(
int
y =
0
;
y <
borderedSize;
y +=
meshSimplificationIncrement) {
for
(
int
x =
0
;
x <
borderedSize;
x +=
meshSimplificationIncrement) {
int
vertexIndex =
vertexIndicesMap [
x,
y];
Vector2 percent =
new
Vector2 ((
x-
meshSimplificationIncrement) /
(
float
)meshSize,
(
y-
meshSimplificationIncrement) /
(
float
)meshSize);
float
height =
heightCurve.
Evaluate (
heightMap [
x,
y]
) *
heightMultiplier;
Vector3 vertexPosition =
new
Vector3 (
topLeftX +
percent.
x *
meshSizeUnsimplified,
height,
topLeftZ -
percent.
y *
meshSizeUnsimplified);
meshData.
AddVertex (
vertexPosition,
percent,
vertexIndex);
if
(
x <
borderedSize -
1
&&
y <
borderedSize -
1
) {
int
a =
vertexIndicesMap [
x,
y];
int
b =
vertexIndicesMap [
x +
meshSimplificationIncrement,
y];
int
c =
vertexIndicesMap [
x,
y +
meshSimplificationIncrement];
int
d =
vertexIndicesMap [
x +
meshSimplificationIncrement,
y +
meshSimplificationIncrement];
meshData.
AddTriangle (
a,
d,
c);
meshData.
AddTriangle (
d,
a,
b);
}
vertexIndex++;
}
}
return
meshData;
}
Maintenant, un tableau d'indices est généré. Dans celui-ci, les indices représentant la bordure seront négatifs, et les indices qui doivent être affichés par la suite restent les mêmes (positifs).
Il est nécessaire d'être prudent avec le niveau de détail, sans quoi le modèle pourrait avoir un décalage avec les morceaux adjacents.
Il est fortement conseillé de travailler avec une feuille de papier et un crayon pour se représenter concrètement les opérations à effectuer.
Aussi, la classe MeshData est modifiée :
public
class
MeshData {
Vector3[]
vertices;
int
[]
triangles;
Vector2[]
uvs;
Vector3[]
borderVertices;
int
[]
borderTriangles;
int
triangleIndex;
int
borderTriangleIndex;
public
MeshData
(
int
verticesPerLine) {
vertices =
new
Vector3[
verticesPerLine *
verticesPerLine];
uvs =
new
Vector2[
verticesPerLine *
verticesPerLine];
triangles =
new
int
[(
verticesPerLine-
1
)*(
verticesPerLine-
1
)*
6
];
borderVertices =
new
Vector3[
verticesPerLine *
4
+
4
];
borderTriangles =
new
int
[
24
*
verticesPerLine];
}
public
void
AddVertex
(
Vector3 vertexPosition,
Vector2 uv,
int
vertexIndex) {
if
(
vertexIndex <
0
) {
borderVertices [-
vertexIndex -
1
]
=
vertexPosition;
}
else
{
vertices [
vertexIndex]
=
vertexPosition;
uvs [
vertexIndex]
=
uv;
}
}
public
void
AddTriangle
(
int
a,
int
b,
int
c) {
if
(
a <
0
||
b <
0
||
c <
0
) {
borderTriangles [
borderTriangleIndex]
=
a;
borderTriangles [
borderTriangleIndex +
1
]
=
b;
borderTriangles [
borderTriangleIndex +
2
]
=
c;
borderTriangleIndex +=
3
;
}
else
{
triangles [
triangleIndex]
=
a;
triangles [
triangleIndex +
1
]
=
b;
triangles [
triangleIndex +
2
]
=
c;
triangleIndex +=
3
;
}
}
// ...
}
Ainsi, la classe contient maintenant les informations de la bordure. De cette façon, les variables membres ne sont plus publiques et il est donc nécessaire d'ajouter une fonction AddVertex() pour permettre d'ajouter des sommets aux tableaux. Suivant l'index (s'il est négatif ou non), le sommet sera ajouté au tableau des sommets de bordure ou au tableau des sommets du modèle.
De même pour la fonction AddTriangle(), il est nécessaire de vérifier les index passés à la fonction pour savoir quel tableau recevra le triangle.
Finalement, la fonction CalculateNormals() est mise à jour pour prendre en compte les triangles de bordure :
Vector3[]
CalculateNormals
(
) {
Vector3[]
vertexNormals =
new
Vector3[
vertices.
Length];
int
triangleCount =
triangles.
Length /
3
;
for
(
int
i =
0
;
i <
triangleCount;
i++
) {
int
normalTriangleIndex =
i *
3
;
int
vertexIndexA =
triangles [
normalTriangleIndex];
int
vertexIndexB =
triangles [
normalTriangleIndex +
1
];
int
vertexIndexC =
triangles [
normalTriangleIndex +
2
];
Vector3 triangleNormal =
SurfaceNormalFromIndices (
vertexIndexA,
vertexIndexB,
vertexIndexC);
vertexNormals [
vertexIndexA]
+=
triangleNormal;
vertexNormals [
vertexIndexB]
+=
triangleNormal;
vertexNormals [
vertexIndexC]
+=
triangleNormal;
}
int
borderTriangleCount =
borderTriangles.
Length /
3
;
for
(
int
i =
0
;
i <
borderTriangleCount;
i++
) {
int
normalTriangleIndex =
i *
3
;
int
vertexIndexA =
borderTriangles [
normalTriangleIndex];
int
vertexIndexB =
borderTriangles [
normalTriangleIndex +
1
];
int
vertexIndexC =
borderTriangles [
normalTriangleIndex +
2
];
Vector3 triangleNormal =
SurfaceNormalFromIndices (
vertexIndexA,
vertexIndexB,
vertexIndexC);
if
(
vertexIndexA >=
0
) {
vertexNormals [
vertexIndexA]
+=
triangleNormal;
}
if
(
vertexIndexB >=
0
) {
vertexNormals [
vertexIndexB]
+=
triangleNormal;
}
if
(
vertexIndexC >=
0
) {
vertexNormals [
vertexIndexC]
+=
triangleNormal;
}
}
for
(
int
i =
0
;
i <
vertexNormals.
Length;
i++
) {
vertexNormals [
i].
Normalize (
);
}
return
vertexNormals;
}
La fonction SurfaceNormalFromIndices() doit aussi gérer le nouveau cas d'indice négatifs :
Vector3 SurfaceNormalFromIndices
(
int
indexA,
int
indexB,
int
indexC) {
Vector3 pointA =
(
indexA <
0
)?
borderVertices[-
indexA-
1
]
:
vertices [
indexA];
Vector3 pointB =
(
indexB <
0
)?
borderVertices[-
indexB-
1
]
:
vertices [
indexB];
Vector3 pointC =
(
indexC <
0
)?
borderVertices[-
indexC-
1
]
:
vertices [
indexC];
Vector3 sideAB =
pointB -
pointA;
Vector3 sideAC =
pointC -
pointA;
return
Vector3.
Cross (
sideAB,
sideAC).
normalized;
}
Enfin, la génération des données des morceaux de terrain doit être modifiée afin de générer des cartes légèrement plus grandes :
MapData GenerateMapData
(
Vector2 centre) {
float
[,]
noiseMap =
Noise.
GenerateNoiseMap (
mapChunkSize +
2
,
mapChunkSize +
2
,
seed,
noiseScale,
octaves,
persistance,
lacunarity,
centre +
offset,
normalizeMode);
// ...
Cette modification demande aussi un changement de la variable mapChunkSize :
public
const
int
mapChunkSize =
239
;
afin de rester divisible par l'incrément lors de la simplification du modèle.
IV. Commenter▲
Vous pouvez commenter et donner vos avis dans la discussion associée sur le forum.