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 - Shader des textures
III. Résumé▲
Dans cet épisode, vous allez remplacer les couleurs définies lors du précédent épisode par des textures.
III-A. Mélange▲
Une première étape pour avoir un résultat convaincant est d'ajouter le mélange de deux couches. Dans le script TextureData.cs, ajoutez une variable baseBlends :
[Range(
0
,
1
)]
public
float
blendBlends;
Cette nouvelle variable est transférée au shader :
material.
SetFloatArray (
"baseBlends"
,
baseBends);
Évidemment, vous devez modifier le shader pour correspondre à ces nouvelles données :
float
baseBlends[maxLayerCount];
Ensuite, modifiez la façon de calculer la couleur finale pour prendre en compte les nouvelles valeurs :
float
drawStrength =
inverseLerp
(-
baseBlends[i]/
2
-
epsilon, baseBlends[i]/
2
, heightPercent -
baseStartHeights[i]);
Une variable epsilon, définie à la valeur 1E-4, est ajoutée afin d'éviter que les valeurs A et B passées à la fonction inverseLerp() ne soient toutes les deux à zéro, provoquant ainsi une division par zéro.
Pour tester, il suffit de définir le nouveau tableau dans l'éditeur.
III-B. Textures▲
Nous allons utiliser un ensemble de textures à appliquer sur le terrain. Vous pouvez télécharger ce pack de textures ici. Importez les textures dans le projet, dans un sous-dosssier « Textures ».
III-B-1. Implémentation de test▲
Pour commencer, nous allons ajouter une seule texture et faire en sorte de l'appliquer correctement à notre terrain.
Dans le shader, vous devez ajouter deux propriétés :
Properties {
textTexture
(
"Texture"
, 2D
) =
"white"
{}
testScale
(
"Scale"
, Float) =
1
}
Dont les valeurs sont rendues accessibles dans le code du shader grâce à deux nouvelles variables :
sampler2D testTexture;
float
testScale;
Le nom de ces variables doit correspondre au nom de la propriété.
Ensuite, vous pouvez utiliser la texture pour calculer la couleur à utiliser :
o.Albedo =
tex2D
(
testTexture, IN.worldPos.xz/
testScale);
La fonction tex2D() permet de lire la texture en un point donné.
En utilisant les coordonnées xz, nous pouvons constater des étirements de la texture. Il en est de même si nous utilisons les coordonnées xy ou yz mais en des cas différents. La solution est donc de combiner ces coordonnées en utilisant la normale du terrain. Cette technique s'appelle « tri planar mapping ».
Ainsi, le calcul de la couleur devient :
struct Input {
float3
worldPos;
float3
worldNormal;
}
;
float3
scaledWorldPos =
IN.wortldPos /
testScale;
float
blendAxerss =
abs
(
IN.worldNormal);
float3
xProjection =
tex2D
(
testTexture, scaledWorldPos.yz) *
blendAxes.x;
float3
yProjection =
tex2D
(
testTexture, scaledWorldPos.xz) *
blendAxes.y;
float3
zProjection =
tex2D
(
testTexture, scaledWorldPos.xy) *
blendAxes.z;
o.Albedo =
xProjection +
yProjection +
zProjection;
La structure des données entrantes Input est modifiée pour recevoir la normale en plus de la position monde du pixel.
On remarque que la texture est plus claire après l'implémentation de la nouvelle méthode de calcul. En effet, l'addition des projections peut dépasser 1. Vous pouvez corriger cela, de cette façon :
float
blendAxerss =
abs
(
IN.worldNormal);
blendAxes /=
blendAxes.x +
blendAxes.y +
blendAxes.z;
III-C. Refactorisation▲
Au lieu d'avoir plusieurs tableaux dans TextureData.cs à transférer au shader, nous souhaitons n'avoir qu'un seul tableau contenant toutes les informations. Pour cela, une nouvelle classe Layer est créée :
[System.Serializable]
public
class
Layer {
public
Texture2D texture;
public
Color tint;
[Range(
0
,
1
)]
public
float
tintStrength;
[Range(
0
,
1
)]
public
float
startHeight;
[Range(
0
,
1
)]
public
float
blendStrength;
public
float
textureScale;
}
Ce faisant, il faut déclarer un tableau de Layer dans la classe TextureData remplaçant l'ensemble des tableaux précédemment définis :
public
Layer[]
layers;
Évidemment, la fonction ApplyToMaterial() doit être modifiée pour correspondre au nouveau code :
public
void
ApplyToMaterial
(
Material material) {
material.
SetInt (
"layerCount"
,
layers.
Length);
material.
SetColorArray (
"baseColours"
,
layers.
Select
(
x =>
x.
tint).
ToArray
(
));
material.
SetFloatArray (
"baseStartHeights"
,
layers.
Select
(
x =>
x.
startHeight).
ToArray
(
));
material.
SetFloatArray (
"baseBlends"
,
layers.
Select
(
x =>
x.
blendStrength).
ToArray
(
));
material.
SetFloatArray (
"baseColourStrength"
,
layers.
Select
(
x =>
x.
tintStrength).
ToArray
(
));
material.
SetFloatArray (
"baseTextureScales"
,
layers.
Select
(
x =>
x.
textureScale).
ToArray
(
));
UpdateMeshHeights (
material,
savedMinHeight,
savedMaxHeight);
}
La variable baseColourCount est renommée layerCount pour que son nom corresponde à la situation. Cette modification doit être rapportée dans le shader.
Linq et ses expressions lambdas sont utilisées pour convertir un champ d'une structure contenue dans un tableau à un tableau ne contenant que ce champ.
Finalement, le shader doit être modifié pour correspondre au changement du nom des variables.
III-D. Gestion des textures▲
Dans la fonction ApplyToMaterial(), vous devez envoyer les textures définies dans le tableau de Layer au shader. Pour ce cas, il est obligatoire d'utiliser une boucle, car il n'existe pas de méthode pour envoyer un tableau de ce type en un appel de fonction. Par contre, il est possible d'utiliser un Texture2DArray :
const
int
textureSize =
512
;
const
TextureFormat textureFormat =
TextureFormat.
RGB565;
Texture2DArray GenerateTextureArray
(
Texture2D[]
textures) {
Texture2DArray textureArray =
new
Texture2DArray (
textureSize,
textureSize,
textures.
Length,
textureFormat,
true
);
for
(
int
i =
0
;
i <
textures.
Length;
i++
) {
textureArray.
SetPixels (
textures [
i].
GetPixels (
),
i);
}
textureArray.
Apply (
);
return
textureArray;
}
Dans ApplyToMaterial(), il suffit d'appeler la nouvelle fonction :
Texture2DArray texturesArray =
GenerateTextureArray (
layers.
Select (
x =>
x.
texture).
ToArray (
));
material.
SetTexture (
"baseTextures"
,
texturesArray);
Ensuite, il faut recevoir les données dans le shader :
UNITY_DECLARE_TEX2DARRAY
(
baseTextures);
Pour que les textures puissent être lues par le shader, il faut modifier les propriétés des images dans Unity.
En sélectionnant tous les fichiers utilisés comme source pour les textures, cochez :
- Advanced → Read/Write Enabled
- Override for PC, Mac & Linux Standalone
Définissez la taille maximale (« Max Size ») à 512 et le format à « RGB 16 bit ».
Finalement, il ne reste plus qu'à utiliser les textures pour calculer la couleur finale :
float3
triplanar
(
float3
worldPos, float
scale, float3
blendAxes, int
textureIndex) {
float3
scaledWorldPos =
worldPos /
scale;
float3
xProjection =
UNITY_SAMPLE_TEX2DARRAY
(
baseTextures, float3
(
scaledWorldPos.y, scaledWorldPos.z, textureIndex)) *
blendAxes.x;
float3
yProjection =
UNITY_SAMPLE_TEX2DARRAY
(
baseTextures, float3
(
scaledWorldPos.x, scaledWorldPos.z, textureIndex)) *
blendAxes.y;
float3
zProjection =
UNITY_SAMPLE_TEX2DARRAY
(
baseTextures, float3
(
scaledWorldPos.x, scaledWorldPos.y, textureIndex)) *
blendAxes.z;
return
xProjection +
yProjection +
zProjection;
}
void
surf (
Input IN, inout SurfaceOutputStandard o) {
float
heightPercent =
inverseLerp
(
minHeight,maxHeight, IN.worldPos.y);
float3
blendAxes =
abs
(
IN.worldNormal);
blendAxes /=
blendAxes.x +
blendAxes.y +
blendAxes.z;
for
(
int
i =
0
; i <
layerCount; i ++
) {
float
drawStrength =
inverseLerp
(-
baseBlends[i]/
2
-
epsilon, baseBlends[i]/
2
, heightPercent -
baseStartHeights[i]);
float3
baseColour =
baseColours[i] *
baseColourStrength[i];
float3
textureColour =
triplanar
(
IN.worldPos, baseTextureScales[i], blendAxes, i) *
(
1
-
baseColourStrength[i]);
o.Albedo =
o.Albedo *
(
1
-
drawStrength) +
(
baseColour+
textureColour) *
drawStrength;
}
}
IV. Commenter▲
Vous pouvez commenter et donner vos avis dans la discussion associée sur le forum.