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.



