There is a method that allows to draw the custom GUI for the terrain layer.:
But there is no documentation and not so much examples how to use this method properly, one of them:
I try to add support for standalone roughness maps (not as alpha channel of albedo) for terrain material:

Script:
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEditor;
public class TerrainRoughness : ShaderGUI, ITerrainLayerCustomUI
{
MaterialPropertyBlock _MaterialPropertyBlock = new MaterialPropertyBlock();
bool ITerrainLayerCustomUI.OnTerrainLayerGUI(TerrainLayer terrainLayer, Terrain terrain)
{
terrain.GetSplatMaterialPropertyBlock(_MaterialPropertyBlock);
TerrainLayer[] terrainLayers = terrain.terrainData.terrainLayers;
Texture2D t = EditorGUILayout.ObjectField(styles.albedoMapTexture, terrainLayer.diffuseTexture, typeof(Texture2D), false) as Texture2D;
Texture2D tt = EditorGUILayout.ObjectField(styles.normalMapTexture, terrainLayer.normalMapTexture, typeof(Texture2D), false) as Texture2D;
for (int i = 0; i < terrainLayers.Length; i++)
{
if (terrainLayers[i].name == terrainLayer.name)
{
terrainLayer.maskMapTexture = EditorGUILayout.ObjectField(styles.roughnessMapTexture, terrainLayer.maskMapTexture, typeof(Texture2D), false) as Texture2D;
_MaterialPropertyBlock.SetTexture("_RoughnessMapCustom" + i.ToString(), terrainLayer.maskMapTexture);
}
}
terrain.SetSplatMaterialPropertyBlock(_MaterialPropertyBlock);
return true;
}
public TerrainRoughness(){}
private Material target;
private MaterialEditor editor;
private MaterialProperty[] properties;
public override void OnGUI(MaterialEditor editor, MaterialProperty[] properties)
{
target = (Material)editor.target;
this.editor = editor;
this.properties = properties;
this.editor.serializedObject.Update();
this.editor.serializedObject.ApplyModifiedProperties();
}
private class StylesLayer
{
public readonly GUIContent normalMapTexture = new GUIContent("Normal Map");
public readonly GUIContent albedoMapTexture = new GUIContent("Albedo Map");
public readonly GUIContent roughnessMapTexture = new GUIContent("Roughness Map");
}
static StylesLayer s_Styles = null;
private static StylesLayer styles { get { if (s_Styles == null) s_Styles = new StylesLayer(); return s_Styles; } }
}
And some modifications to TerrainSplatmapCommon.cginc , like this:
#ifndef TERRAIN_SPLATMAP_COMMON_CGINC_INCLUDED
#define TERRAIN_SPLATMAP_COMMON_CGINC_INCLUDED
// Since 2018.3 we changed from _TERRAIN_NORMAL_MAP to _NORMALMAP to save 1 keyword.
// Since 2019.2 terrain keywords are changed to local keywords so it doesn't really matter. You can use both.
#if defined(_NORMALMAP) && !defined(_TERRAIN_NORMAL_MAP)
#define _TERRAIN_NORMAL_MAP
#elif !defined(_NORMALMAP) && defined(_TERRAIN_NORMAL_MAP)
#define _NORMALMAP
#endif
#if defined(SHADER_API_GLCORE) || defined(SHADER_API_GLES3) || defined(SHADER_API_GLES)
// GL doesn't support sperating the samplers from the texture object
#undef TERRAIN_USE_SEPARATE_VERTEX_SAMPLER
#else
#define TERRAIN_USE_SEPARATE_VERTEX_SAMPLER
#endif
struct Input
{
float4 tc;
#ifndef TERRAIN_BASE_PASS
UNITY_FOG_COORDS(0) // needed because finalcolor oppresses fog code generation.
#endif
};
sampler2D _Control;
float4 _Control_ST;
float4 _Control_TexelSize;
sampler2D _Splat0, _Splat1, _Splat2, _Splat3;
float4 _Splat0_ST, _Splat1_ST, _Splat2_ST, _Splat3_ST;
sampler2D _RoughnessMapCustom0, _RoughnessMapCustom1, _RoughnessMapCustom2, _RoughnessMapCustom3;
#if defined(UNITY_INSTANCING_ENABLED) && !defined(SHADER_API_D3D11_9X)
// Some drivers have undefined behaviors when samplers are used from the vertex shader
// with anisotropic filtering enabled. This causes some artifacts on some devices. To be
// sure to avoid this we use the vertex_linear_clamp_sampler sampler to sample terrain
// maps from the VS when we can.
#if defined(TERRAIN_USE_SEPARATE_VERTEX_SAMPLER)
UNITY_DECLARE_TEX2D(_TerrainHeightmapTexture);
UNITY_DECLARE_TEX2D(_TerrainNormalmapTexture);
SamplerState sampler__TerrainNormalmapTexture;
SamplerState vertex_linear_clamp_sampler;
#else
sampler2D _TerrainHeightmapTexture;
sampler2D _TerrainNormalmapTexture;
#endif
float4 _TerrainHeightmapRecipSize; // float4(1.0f/width, 1.0f/height, 1.0f/(width-1), 1.0f/(height-1))
float4 _TerrainHeightmapScale; // float4(hmScale.x, hmScale.y / (float)(kMaxHeight), hmScale.z, 0.0f)
#endif
UNITY_INSTANCING_BUFFER_START(Terrain)
UNITY_DEFINE_INSTANCED_PROP(float4, _TerrainPatchInstanceData) // float4(xBase, yBase, skipScale, ~)
UNITY_INSTANCING_BUFFER_END(Terrain)
#ifdef _NORMALMAP
sampler2D _Normal0, _Normal1, _Normal2, _Normal3;
float _NormalScale0, _NormalScale1, _NormalScale2, _NormalScale3;
#endif
#ifdef _ALPHATEST_ON
sampler2D _TerrainHolesTexture;
void ClipHoles(float2 uv)
{
float hole = tex2D(_TerrainHolesTexture, uv).r;
clip(hole == 0.0f ? -1 : 1);
}
#endif
#if defined(TERRAIN_BASE_PASS) && defined(UNITY_PASS_META)
// When we render albedo for GI baking, we actually need to take the ST
float4 _MainTex_ST;
#endif
void SplatmapVert(inout appdata_full v, out Input data)
{
UNITY_INITIALIZE_OUTPUT(Input, data);
#if defined(UNITY_INSTANCING_ENABLED) && !defined(SHADER_API_D3D11_9X)
float2 patchVertex = v.vertex.xy;
float4 instanceData = UNITY_ACCESS_INSTANCED_PROP(Terrain, _TerrainPatchInstanceData);
float4 uvscale = instanceData.z * _TerrainHeightmapRecipSize;
float4 uvoffset = instanceData.xyxy * uvscale;
uvoffset.xy += 0.5f * _TerrainHeightmapRecipSize.xy;
float2 sampleCoords = (patchVertex.xy * uvscale.xy + uvoffset.xy);
#if defined(TERRAIN_USE_SEPARATE_VERTEX_SAMPLER)
float hm = UnpackHeightmap(_TerrainHeightmapTexture.SampleLevel(vertex_linear_clamp_sampler, sampleCoords, 0));
#else
float hm = UnpackHeightmap(tex2Dlod(_TerrainHeightmapTexture, float4(sampleCoords, 0, 0)));
#endif
v.vertex.xz = (patchVertex.xy + instanceData.xy) * _TerrainHeightmapScale.xz * instanceData.z; //(x + xBase) * hmScale.x * skipScale;
v.vertex.y = hm * _TerrainHeightmapScale.y;
v.vertex.w = 1.0f;
v.texcoord.xy = (patchVertex.xy * uvscale.zw + uvoffset.zw);
v.texcoord3 = v.texcoord2 = v.texcoord1 = v.texcoord;
#ifdef TERRAIN_INSTANCED_PERPIXEL_NORMAL
v.normal = float3(0, 1, 0); // TODO: reconstruct the tangent space in the pixel shader. Seems to be hard with surface shader especially when other attributes are packed together with tSpace.
data.tc.zw = sampleCoords;
#else
#if defined(TERRAIN_USE_SEPARATE_VERTEX_SAMPLER)
float3 nor = _TerrainNormalmapTexture.SampleLevel(vertex_linear_clamp_sampler, sampleCoords, 0).xyz;
#else
float3 nor = tex2Dlod(_TerrainNormalmapTexture, float4(sampleCoords, 0, 0)).xyz;
#endif
v.normal = 2.0f * nor - 1.0f;
#endif
#endif
v.tangent.xyz = cross(v.normal, float3(0,0,1));
v.tangent.w = -1;
data.tc.xy = v.texcoord.xy;
#ifdef TERRAIN_BASE_PASS
#ifdef UNITY_PASS_META
data.tc.xy = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
#endif
#else
float4 pos = UnityObjectToClipPos(v.vertex);
UNITY_TRANSFER_FOG(data, pos);
#endif
}
#ifndef TERRAIN_BASE_PASS
#ifdef TERRAIN_STANDARD_SHADER
void SplatmapMix(Input IN, half4 defaultAlpha, out half4 splat_control, out half weight, out fixed4 mixedDiffuse, inout fixed3 mixedNormal, inout float mixedRoughness)
#else
void SplatmapMix(Input IN, out half4 splat_control, out half weight, out fixed4 mixedDiffuse, inout fixed3 mixedNormal)
#endif
{
#ifdef _ALPHATEST_ON
ClipHoles(IN.tc.xy);
#endif
// adjust splatUVs so the edges of the terrain tile lie on pixel centers
float2 splatUV = (IN.tc.xy * (_Control_TexelSize.zw - 1.0f) + 0.5f) * _Control_TexelSize.xy;
splat_control = tex2D(_Control, splatUV);
weight = dot(splat_control, half4(1,1,1,1));
#if !defined(SHADER_API_MOBILE) && defined(TERRAIN_SPLAT_ADDPASS)
clip(weight == 0.0f ? -1 : 1);
#endif
// Normalize weights before lighting and restore weights in final modifier functions so that the overal
// lighting result can be correctly weighted.
splat_control /= (weight + 1e-3f);
float2 uvSplat0 = TRANSFORM_TEX(IN.tc.xy, _Splat0);
float2 uvSplat1 = TRANSFORM_TEX(IN.tc.xy, _Splat1);
float2 uvSplat2 = TRANSFORM_TEX(IN.tc.xy, _Splat2);
float2 uvSplat3 = TRANSFORM_TEX(IN.tc.xy, _Splat3);
mixedDiffuse = 0.0f;
mixedRoughness = 0.0f;
#ifdef TERRAIN_STANDARD_SHADER
mixedDiffuse += splat_control.r * tex2D(_Splat0, uvSplat0) * half4(1.0, 1.0, 1.0, defaultAlpha.r);
mixedDiffuse += splat_control.g * tex2D(_Splat1, uvSplat1) * half4(1.0, 1.0, 1.0, defaultAlpha.g);
mixedDiffuse += splat_control.b * tex2D(_Splat2, uvSplat2) * half4(1.0, 1.0, 1.0, defaultAlpha.b);
mixedDiffuse += splat_control.a * tex2D(_Splat3, uvSplat3) * half4(1.0, 1.0, 1.0, defaultAlpha.a);
mixedRoughness += splat_control.r * tex2D(_RoughnessMapCustom0, uvSplat0) * half4(1.0, 1.0, 1.0, defaultAlpha.r);
mixedRoughness += splat_control.g * tex2D(_RoughnessMapCustom1, uvSplat1) * half4(1.0, 1.0, 1.0, defaultAlpha.g);
mixedRoughness += splat_control.b * tex2D(_RoughnessMapCustom2, uvSplat2) * half4(1.0, 1.0, 1.0, defaultAlpha.b);
mixedRoughness += splat_control.a * tex2D(_RoughnessMapCustom3, uvSplat3) * half4(1.0, 1.0, 1.0, defaultAlpha.a);
#else
mixedDiffuse += splat_control.r * tex2D(_Splat0, uvSplat0);
mixedDiffuse += splat_control.g * tex2D(_Splat1, uvSplat1);
mixedDiffuse += splat_control.b * tex2D(_Splat2, uvSplat2);
mixedDiffuse += splat_control.a * tex2D(_Splat3, uvSplat3);
#endif
#ifdef _NORMALMAP
mixedNormal = UnpackNormalWithScale(tex2D(_Normal0, uvSplat0), _NormalScale0) * splat_control.r;
mixedNormal += UnpackNormalWithScale(tex2D(_Normal1, uvSplat1), _NormalScale1) * splat_control.g;
mixedNormal += UnpackNormalWithScale(tex2D(_Normal2, uvSplat2), _NormalScale2) * splat_control.b;
mixedNormal += UnpackNormalWithScale(tex2D(_Normal3, uvSplat3), _NormalScale3) * splat_control.a;
#if defined(SHADER_API_SWITCH)
mixedNormal.z += UNITY_HALF_MIN; // to avoid nan after normalizing
#else
mixedNormal.z += 1e-5f; // to avoid nan after normalizing
#endif
#endif
#if defined(INSTANCING_ON) && defined(SHADER_TARGET_SURFACE_ANALYSIS) && defined(TERRAIN_INSTANCED_PERPIXEL_NORMAL)
mixedNormal = float3(0, 0, 1); // make sure that surface shader compiler realizes we write to normal, as UNITY_INSTANCING_ENABLED is not defined for SHADER_TARGET_SURFACE_ANALYSIS.
#endif
#if defined(UNITY_INSTANCING_ENABLED) && !defined(SHADER_API_D3D11_9X) && defined(TERRAIN_INSTANCED_PERPIXEL_NORMAL)
#if defined(TERRAIN_USE_SEPARATE_VERTEX_SAMPLER)
float3 geomNormal = normalize(_TerrainNormalmapTexture.Sample(sampler__TerrainNormalmapTexture, IN.tc.zw).xyz * 2 - 1);
#else
float3 geomNormal = normalize(tex2D(_TerrainNormalmapTexture, IN.tc.zw).xyz * 2 - 1);
#endif
#ifdef _NORMALMAP
float3 geomTangent = normalize(cross(geomNormal, float3(0, 0, 1)));
float3 geomBitangent = normalize(cross(geomTangent, geomNormal));
mixedNormal = mixedNormal.x * geomTangent
+ mixedNormal.y * geomBitangent
+ mixedNormal.z * geomNormal;
#else
mixedNormal = geomNormal;
#endif
mixedNormal = mixedNormal.xzy;
#endif
}
#ifndef TERRAIN_SURFACE_OUTPUT
#define TERRAIN_SURFACE_OUTPUT SurfaceOutput
#endif
void SplatmapFinalColor(Input IN, TERRAIN_SURFACE_OUTPUT o, inout fixed4 color)
{
color *= o.Alpha;
#ifdef TERRAIN_SPLAT_ADDPASS
UNITY_APPLY_FOG_COLOR(IN.fogCoord, color, fixed4(0,0,0,0));
#else
UNITY_APPLY_FOG(IN.fogCoord, color);
#endif
}
void SplatmapFinalPrepass(Input IN, TERRAIN_SURFACE_OUTPUT o, inout fixed4 normalSpec)
{
normalSpec *= o.Alpha;
}
void SplatmapFinalGBuffer(Input IN, TERRAIN_SURFACE_OUTPUT o, inout half4 outGBuffer0, inout half4 outGBuffer1, inout half4 outGBuffer2, inout half4 emission)
{
UnityStandardDataApplyWeightToGbuffer(outGBuffer0, outGBuffer1, outGBuffer2, o.Alpha);
emission *= o.Alpha;
}
#endif // TERRAIN_BASE_PASS
#endif // TERRAIN_SPLATMAP_COMMON_CGINC_INCLUDED
and it works, but when I reload scene, layers are not loaded properly - I need to click the terrain layer in Terrain Inspector and point mouse cursor over terrain, then layers are loaded again. Does someone has experience with OnTerrainLayerGUI, how to use this method properly for custom terrain GUI ?
Thanks in advance.