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

Shader des couleurs

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 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 :

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

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

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

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

 
Sélectionnez
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() :

 
Sélectionnez
textureData.UpdateMeshHeights (terrainMaterial, terrainData.minHeight, terrainData.maxHeight);

Maintenant que les données sont disponibles, il ne reste plus qu'à les utiliser dans le shader :

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

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

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

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

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

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

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 et 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.