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 couleurs
III. Résumé▲
Dans cet épisode, vous allez implémenter les shaders permettant ainsi de personnaliser votre rendu. Pour ce premier épisode, le shader implémentera la coloration du modèle 3D.
III-A. Implémentation▲
Ajoutez un nouveau Shader de type « Standard Surface Shader » et nommez-le « Terrain ». Pour utiliser ce nouveau shader, sélectionnez votre matériau « Mesh Mat » et assignez-lui le shader nouvellement créé.
Vous remarquez que le shader contient déjà du code :
Shader "Custom/Terrain" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white"  {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader {
        Tags { "RanderType"="Opaque" }
        LOD 200
        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows
        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0
        sampler2D _MainTex;
        struct Input {
            float3 worldPos;
        };
        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
    void surf (Input IN, input SurfaceOutputStandard o) {
        // Albedo comes from a texture tinted by color
        fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
        o.Albedo = c.rgb;
        // Metallic and smoothness come from slider variables
        o.Metallic = _Metallic;
        o.Smoothness = _Glossiness;
        o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}La fonction surf() est appelée pour chaque pixel visible de notre modèle. Le rôle de la fonction est de définir la couleur que le pixel actuellement traité devra prendre. Pour ce faire, il suffit de donner une valeur à la variable o.Albedo.
Dans notre cas, nous souhaitons que le shader mélange plusieurs textures suivant la hauteur du pixel dans le modèle.
		Nous pouvons utiliser la variable worldPos de la structure Input pour connaître la position du pixel dans le monde :
struct Input {
    float3 worldPos;
};Toutefois, cela ne suffit pas, car il faut aussi connaître la hauteur minimale et la hauteur maximale du terrain, pour obtenir une valeur entre 0 et 1 suivant la hauteur du pixel. Pour cela, deux nouvelles variables sont déclarées :
float minHeight;
float maxHeight;Leur valeur sera renseignée par le script TerrainData.
Le shader par défaut ne convient pas à notre besoin. Les variables définies dans le bloc Properties peuvent être supprimées.
Les variables définies dans ce bloc seront affichées et manipulables dans l'éditeur.
La variable mainTex, représentant une texture, peut aussi être supprimée, car pour le moment nous n'avons pas de texture.
		Finalement, les variables _Glossinness, _Metallic et _Color sont aussi inutilisées.
III-B. Définitions des variables du shader▲
Dans le script TerrainData, ajoutez les définitions suivantes :
public float minHeight {
    get {
        return uniformScale * meshHeightMultiplier * meshHeightCurve.Evaluate (0);
    }
}
public float maxHeight {
    get {
        return uniformScale * meshHeightMultiplier * meshHeightCurve.Evaluate (1);
    }
}Cela permet de récupérer les valeurs nécessaires au fonctionnement du shader.
Toutefois, c'est le script TextureData qui envoie les informations au shader :
public void UpdateMeshHeights(Material material, float minHeight, float maxHeight) {
    material.SetFloat ("minHeight", minHeight);
    material.SetFloat ("maxHeight", maxHeight);
}Cette nouvelle fonction est finalement appelée dans MapGenerator::GenerateMapData() :
textureData.UpdateMeshHeights (terrainMaterial, terrainData.minHeight, terrainData.maxHeight);Maintenant que les données sont disponibles, il ne reste plus qu'à les utiliser dans le shader :
float inverseLerp(float a, float b, float value) {
    return saturate((value-a)/(b-a));
}
void surf (Input IN, inout SurfaceOutputStandard o) {
    float heightPercent = inverseLerp(minHeight,maxHeight, IN.worldPos.y);
    o.Albedo = heightPercent;
}La fonction inverseLerp() permet de calculer l'inverse de l'interpolation linéaire. Dans le cas où la valeur passée est supérieure à b, le résultat est mis à 1, grâce à la fonction saturate.
III-C. Problème avec la compilation▲
Si le script est recompilé, les couleurs ne seront pas correctement affichées. Pour corriger ce défaut, la classe UpdatableData est mise à jour :
using UnityEngine;
using System.Collections;
public class UpdatableData : ScriptableObject {
    public event System.Action OnValuesUpdated;
    public bool autoUpdate;
    protected virtual void OnValidate() {
        if (autoUpdate) {
            UnityEditor.EditorApplication.update += NotifyOfUpdatedValues;
        }
    }
    public void NotifyOfUpdatedValues() {
        UnityEditor.EditorApplication.update -= NotifyOfUpdatedValues;
        if (OnValuesUpdated != null) {
            OnValuesUpdated ();
        }
    }
}Afin de retarder la mise à jour des valeurs, nous utilisons le mécanisme de mise à jour des valeurs de l'éditeur.
Toutefois, cela ne corrige pas le défaut si c'est le shader qui est mis à jour. Il est possible de diminuer cet aspect gênant en modifiant TextureData pour que le bouton « Update » renvoie les valeurs au shader :
float savedMinHeight;
float savedMaxHeight;
public void ApplyToMaterial(Material material) {
    UpdateMeshHeights (material, savedMinHeight, savedMaxHeight);
}
public void UpdateMeshHeights(Material material, float minHeight, float maxHeight) {
    savedMinHeight = minHeight;
    savedMaxHeight = maxHeight;
    material.SetFloat ("minHeight", minHeight);
    material.SetFloat ("maxHeight", maxHeight);
}III-D. Ajout des couleurs▲
Avant d'ajouter la gestion des textures, nous allons faire en sorte que le shader affiche une couleur spécifique suivant la hauteur du terrain. Pour cela, nous définissons des couleurs et les paliers entre chaque couleur, dans la classe TextureData :
using UnityEngine;
using System.Collections;
[CreateAssetMenu()]
public class TextureData : UpdatableData {
    public Color[] baseColours;
    [Range(0,1)]
    public float[] baseStartHeights;
    float savedMinHeight;
    float savedMaxHeight;
    public void ApplyToMaterial(Material material) {
        material.SetInt ("baseColourCount", baseColours.Length);
        material.SetColorArray ("baseColours", baseColours);
        material.SetFloatArray ("baseStartHeights", baseStartHeights);
        UpdateMeshHeights (material, savedMinHeight, savedMaxHeight);
    }
    public void UpdateMeshHeights(Material material, float minHeight, float maxHeight) {
        savedMinHeight = minHeight;
        savedMaxHeight = maxHeight;
        material.SetFloat ("minHeight", minHeight);
        material.SetFloat ("maxHeight", maxHeight);
    }
}On remarque qu'en plus de passer les tableaux au shader, il faut passer le nombre d'éléments les constituant.
Et voici leur utilisation dans le shader :
Shader "Custom/Terrain" {
    Properties {
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
        
        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows
        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0
        const static int maxColourCount = 8;
        int baseColourCount;
        float3 baseColours[maxColourCount];
        float baseStartHeights[maxColourCount];
        float minHeight;
        float maxHeight;
        struct Input {
            float3 worldPos;
        };
        float inverseLerp(float a, float b, float value) {
            return saturate((value-a)/(b-a));
        }
        void surf (Input IN, inout SurfaceOutputStandard o) {
            float heightPercent = inverseLerp(minHeight,maxHeight, IN.worldPos.y);
            for (int i = 0; i < baseColourCount; i ++) {
                float drawStrength = saturate(sign(heightPercent - baseStartHeights[i]));
                o.Albedo = o.Albedo * (1-drawStrength) + baseColours[i] * drawStrength;
            }
        }
        ENDCG
    }
    FallBack "Diffuse"
}Les tableaux doivent être déclarés avec une taille. Celle-ci doit être supérieure au nombre d'éléments effectivement présents dans le tableau.
III-E. Bogue à la sauvegarde▲
Lors de la sauvegarde du projet, les couleurs disparaissent et ne réapparaissent pas, même en cliquant sur le bouton « Update ». Pour corriger cela, il faut indiquer que la scène doit être réinitialisée :
using UnityEngine;
using System.Collections;
using UnityEditor;
[CustomEditor (typeof(UpdatableData), true)]
public class UpdatableDataEditor : Editor {
    public override void OnInspectorGUI ()
    {
        base.OnInspectorGUI ();
        UpdatableData data = (UpdatableData)target;
        if (GUILayout.Button ("Update")) {
            data.NotifyOfUpdatedValues ();
            EditorUtility.SetDirty (target);
        }
    }
    
}IV. Commenter▲
Vous pouvez commenter et donner vos avis dans la discussion associée sur le forum.





