Unity - Génération procédurale de terrain

Shader des textures

L'auteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 :

TextureData.cs
Sélectionnez
[Range(0,1)]
public float blendBlends;

Cette nouvelle variable est transférée au shader :

TextureData.cs
Sélectionnez
material.SetFloatArray ("baseBlends", baseBends);

Évidemment, vous devez modifier le shader pour correspondre à ces nouvelles données :

Terrain.shader
Sélectionnez
float baseBlends[maxLayerCount];

Ensuite, modifiez la façon de calculer la couleur finale pour prendre en compte les nouvelles valeurs :

Terrain.shader
Sélectionnez
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 :

Terrain.shader
Sélectionnez
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 :

Terrain.shader
Sélectionnez
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 :

Terrain.shader
Sélectionnez
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 :

Terrain.shader
Sélectionnez
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 :

Terrain.shader
Sélectionnez
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 :

TextureData.cs
Sélectionnez
[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 :

TextureData.cs
Sélectionnez
public Layer[] layers;

Évidemment, la fonction ApplyToMaterial() doit être modifiée pour correspondre au nouveau code :

TextureData.cs
Sélectionnez
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 :

TextureData.cs
Sélectionnez
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 :

TextureData.cs
Sélectionnez
Texture2DArray texturesArray = GenerateTextureArray (layers.Select (x => x.texture).ToArray ());
material.SetTexture ("baseTextures", texturesArray);

Ensuite, il faut recevoir les données dans le shader :

Terrain.shader
Sélectionnez
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 :

Terrain.shader
Sélectionnez
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.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2018 Unity Technologies. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.