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.