Custom Unity Terrain Material / Shaders

I love Unity – it’s one of those environments that’s actually a pleasure to program in, but its terrain system seems somewhat lacking behind its other features, requiring quite a lot of hacks and workarounds to get desired appearance and functionality (while still having an acceptable level of performance).

For example, at first glance, the inbuilt terrain appears to have only one shader, which only supports diffuse textures. And when you try to search the internet for “custom Unity terrain shader”, say, you find a variety of conflicting information that suggests it’s either not supported, requires exporting the raw terrain to a modelling program, or involves creating a new Terrain shader which is named the same (and therefore overrides) the hidden internal terrain shaders (typically "Hidden/TerrainEngine/Splatmap/Lightmap-FirstPass" or something similarly arcane) – hardly intuitive.

Many of the previous descriptions are outdated and inaccurate but, unfortunately, still appear as top hits in search result pages, and it seems hard to find reliable up-to-date information instead. So, for reference, this blog post will describe my experience of creating custom terrain shaders as of today (Dec 2013), using Unity 4.2.

 

Normal-Mapped/Specular Terrain

Since Unity 4.0, creating bump-mapped/specular terrain is actually pretty straightforward as Unity already comes supplied with a suitable shader. The steps required to apply it are as follows:

1. Create a new material and assign the Nature/Terrain/Bumped Specular shader to it. Note that there won’t be any texture slots on this material – it will use the textures assigned to the terrain itself in the next step.

image

2. Highlight the terrain and, in the inspector, click on the Paintbrush icon. Click Edit Textures –> Add Texture.

image

Add main textures and normal maps for each texture to be used on the terrain (they won’t take effect just yet):

image

You can then use the range of splat tools to paint the various textures onto the terrain.

3. Note the information text in the previous step says you need to apply a normal-mapped material to the terrain, so let’s do that: click on the cog icon on the right hand side and drag the material created in Step One into the “material” slot:

You should notice that your flat, diffuse terrain changes from this:

image

to this:

image

 

Custom Terrain Shaders

Creating bumped / specular shaders as above is fairly straightforward because Unity already comes with a bumped specular terrain shader – you just need to specify a material that uses it in place of the default diffuse terrain shader.

To create an alternative custom terrain shader other than bumped/specular is slightly more complicated – it might help to first see the structure of the inbuilt terrain shaders. You can download the source of all Unity supplied shaders from http://unity3d.com/unity/download/archive

  • The default diffuse terrain shader – Nature/Terrain/Diffuse – can be found in \DefaultResourcesExtra\TerrainShaders\Splats\FirstPass.shader. It has a dependency on Hidden/TerrainEngine/Splatmap/Lightmap-AddPass, which can be found at \DefaultResourcesExtra\TerrainShaders\Splats\AddPass.shader
  • The bumped specular terrain shader – Nature/Terrain/Bumped Specular – can be found in \DefaultResourcesExtra\Nature\Terrain\TerrBumpFirstPass.shader. It has a dependency on Hidden/Nature/Terrain/Bumped Specular AddPass, which can be found at \DefaultResourcesExtra\Nature\Terrain\TerrBumpAddPass.shader

The first thing to notice is the Properties block, which contains definitions for the textures and splat map control texture. These are marked as [HideInInspector] since they will be passed in automatically from the terrain engine rather than set in the material inspector. Also note that the control texture is passed in with a default value of “Red”, which is the channel used to indicate the influence of the Layer 0 texture (hence, by default, the entire terrain is coloured with _Splat0 texture):

// Splat Map Control Texture
[HideInInspector] _Control ("Control (RGBA)", 2D) = "red" {}

// Textures
[HideInInspector] _Splat3 ("Layer 3 (A)", 2D) = "white" {}
[HideInInspector] _Splat2 ("Layer 2 (B)", 2D) = "white" {}
[HideInInspector] _Splat1 ("Layer 1 (G)", 2D) = "white" {}
[HideInInspector] _Splat0 ("Layer 0 (R)", 2D) = "white" {}

// Normal Maps
[HideInInspector] _Normal3 ("Normal 3 (A)", 2D) = "bump" {}
[HideInInspector] _Normal2 ("Normal 2 (B)", 2D) = "bump" {}
[HideInInspector] _Normal1 ("Normal 1 (G)", 2D) = "bump" {}
[HideInInspector] _Normal0 ("Normal 0 (R)", 2D) = "bump" {}

The body of the shader is fairly standard – blending each of the four splat textures based on the corresponding RGBA channel from the control texture and setting that to the material albedo:

fixed3 col;
col  = splat_control.r * tex2D (_Splat0, IN.uv_Splat0).rgb;
col += splat_control.g * tex2D (_Splat1, IN.uv_Splat1).rgb;
col += splat_control.b * tex2D (_Splat2, IN.uv_Splat2).rgb;
col += splat_control.a * tex2D (_Splat3, IN.uv_Splat3).rgb;
o.Albedo = col;

The last bit to be careful of is near the bottom: the shader includes two “dependencies” – additional shaders – as follows:

Dependency "AddPassShader" = "MyShaders/Terrain/Editor/AddPass"
Dependency "BaseMapShader" = "MyShaders/Terrain/Editor/BaseMap"

The “Dependency” property appears to be unique to terrain shaders, and is not described anywhere in Unity’s shaderlab documentation, but here’s my findings:

AddPassShader

Examining the code of the shader specified in the “AddPassShader”, you’ll find the actual surface shader function is identical to that in the “FirstPassShader”. The differences between the two shaders are subtle:

  • FirstPassShader specifies “Queue” = “Geometry-100”, whereas AddPassShader is drawn in a later renderqueue: "Queue" = "Geometry-99"
  • AddPassShader specifies "IgnoreProjector"="True"
  • Rather than using the default blendmode, which would overwrite the output of previous, AddPassShader is an additive pass, specified using the decal:add parameter following the surface shader pragma directive.

Other than that, they’re pretty much identical. From empirical evidence and a bit of logical thinking, the reason for these two shaders and their slight differences can be explained – the “FirstPass” shader (i.e. the one used by the material assigned to the terrain) only has four texture slots available. If you assign more than four textures to the terrain, the “AddPassShader” is called by the terrain engine (and its _Control, _SplatX, and _NormalX textures assigned) for each additional batch of four textures assigned to the terrain, and its output is additively blended with the first pass. So:

  • Textures 1-4 rendered with FirstPassShader
  • Textures 5-8 rendered with AddPassShader (if necessary)
  • Textures 9-12 rendered with AddPassShader (if necessary)
  • etc…

If you specify four or less terrain textures, the Dependency “AddPassShader “directive is not required as the shader will never be called.

BaseMapShader

The FirstPassShader and AddPassShader pass the terrain control texture and splat textures to the shader and blend them in the shader itself. The problem with this approach is that it is relatively expensive on the GPU, and is not necessarily supported by older graphics cards. To compensate for this, Unity also combines all terrain splat textures (based on the control texture) into a single combined base texture (a bit like “baking” a lightmap), with resolution specified by the "Base Texture Resolution" parameter in the terrain inspector. This base texture is then passed to the shader so it can be used as a fallback by old graphics cards that can’t blend the textures in the FirstPassShader, and also for any terrain that is further away than "Base Map Dist." (also specified in the inspector). This single texture is rendered using the Dependency "BaseMapShader" shader.

It receives the base texture in its  _MainTex parameter, and an additional _Colour parameter, both of which automatically set from the FirstPassShader.

 

Example Custom Terrain Shader : Toon Outlined Terrain

Here’s an example based on the above – it has a firstpass and addpass shader that blend textures as normal, but with a ramped toon lighting model. They then specify UsePass to apply a second pass which adds the outline from the Toon/Lit Outlinedshader. The basemap shader simply uses the Toon Lit Oulined shader directly on the basemap texture.

ToonTerrainFirstPass.shader

Shader "Custom/ToonTerrainFirstPass" {
Properties {

	// Control Texture ("Splat Map")
	[HideInInspector] _Control ("Control (RGBA)", 2D) = "red" {}
	
	// Terrain textures - each weighted according to the corresponding colour
	// channel in the control texture
	[HideInInspector] _Splat3 ("Layer 3 (A)", 2D) = "white" {}
	[HideInInspector] _Splat2 ("Layer 2 (B)", 2D) = "white" {}
	[HideInInspector] _Splat1 ("Layer 1 (G)", 2D) = "white" {}
	[HideInInspector] _Splat0 ("Layer 0 (R)", 2D) = "white" {}
	
	// Used in fallback on old cards & also for distant base map
	[HideInInspector] _MainTex ("BaseMap (RGB)", 2D) = "white" {}
	[HideInInspector] _Color ("Main Color", Color) = (1,1,1,1)
	
	// Let the user assign a lighting ramp to be used for toon lighting
	_Ramp ("Toon Ramp (RGB)", 2D) = "gray" {}
	
	// Colour of toon outline
	_OutlineColor ("Outline Color", Color) = (0,0,0,1)
	_Outline ("Outline width", Range (.002, 0.03)) = .005
}
	
SubShader {
	Tags {
		"SplatCount" = "4"
		"Queue" = "Geometry-100"
		"RenderType" = "Opaque"
	}
	
	// TERRAIN PASS	
	CGPROGRAM
	#pragma surface surf ToonRamp exclude_path:prepass 

	// Access the Shaderlab properties
	uniform sampler2D _Control;
	uniform sampler2D _Splat0,_Splat1,_Splat2,_Splat3;
	uniform fixed4 _Color;
	uniform sampler2D _Ramp;

	// Custom lighting model that uses a texture ramp based
	// on angle between light direction and normal
	inline half4 LightingToonRamp (SurfaceOutput s, half3 lightDir, half atten)
	{
		#ifndef USING_DIRECTIONAL_LIGHT
		lightDir = normalize(lightDir);
		#endif
		// Wrapped lighting
		half d = dot (s.Normal, lightDir) * 0.5 + 0.5;
		// Applied through ramp
		half3 ramp = tex2D (_Ramp, float2(d,d)).rgb;
		half4 c;
		c.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2);
		c.a = 0;
		return c;
	}

	// Surface shader input structure
	struct Input {
		float2 uv_Control : TEXCOORD0;
		float2 uv_Splat0 : TEXCOORD1;
		float2 uv_Splat1 : TEXCOORD2;
		float2 uv_Splat2 : TEXCOORD3;
		float2 uv_Splat3 : TEXCOORD4;
	};

	// Surface Shader function
	void surf (Input IN, inout SurfaceOutput o) {
		fixed4 splat_control = tex2D (_Control, IN.uv_Control);
		fixed3 col;
		col  = splat_control.r * tex2D (_Splat0, IN.uv_Splat0).rgb;
		col += splat_control.g * tex2D (_Splat1, IN.uv_Splat1).rgb;
		col += splat_control.b * tex2D (_Splat2, IN.uv_Splat2).rgb;
		col += splat_control.a * tex2D (_Splat3, IN.uv_Splat3).rgb;
		o.Albedo = col * _Color;
		o.Alpha = 0.0;
	}
	ENDCG

	// Use the Outline Pass from the default Toon shader
	UsePass "Toon/Basic Outline/OUTLINE"

} // End SubShader

// Specify dependency shaders	
Dependency "AddPassShader" = "Custom/ToonTerrainAddPass"
Dependency "BaseMapShader" = "Toon/Lighted Outline"

// Fallback to Diffuse
Fallback "Diffuse"

} // Ehd Shader

 

ToonTerrainAddPass.shader

Shader "Custom/ToonTerrainAddPass" {
Properties {

	// Control Texture ("Splat Map")
	[HideInInspector] _Control ("Control (RGBA)", 2D) = "red" {}
	
	// Terrain textures - each weighted according to the corresponding colour
	// channel in the control texture
	[HideInInspector] _Splat3 ("Layer 3 (A)", 2D) = "white" {}
	[HideInInspector] _Splat2 ("Layer 2 (B)", 2D) = "white" {}
	[HideInInspector] _Splat1 ("Layer 1 (G)", 2D) = "white" {}
	[HideInInspector] _Splat0 ("Layer 0 (R)", 2D) = "white" {}
	
	// Used in fallback on old cards & also for distant base map
	[HideInInspector] _MainTex ("BaseMap (RGB)", 2D) = "white" {}
	[HideInInspector] _Color ("Main Color", Color) = (1,1,1,1)
	
	// Let the user assign a lighting ramp to be used for toon lighting
	_Ramp ("Toon Ramp (RGB)", 2D) = "gray" {}
	
	// Colour of toon outline
	_OutlineColor ("Outline Color", Color) = (0,0,0,1)
	_Outline ("Outline width", Range (.002, 0.03)) = .005
}
	
SubShader {
	Tags {
		"SplatCount" = "4"
		"Queue" = "Geometry-99"
		"RenderType" = "Opaque"
		"IgnoreProjector"="True"
	}
	
	// TERRAIN PASS	
	CGPROGRAM
	#pragma surface surf ToonRamp decal:add

	// Access the Shaderlab properties
	uniform sampler2D _Control;
	uniform sampler2D _Splat0,_Splat1,_Splat2,_Splat3;
	uniform fixed4 _Color;
	uniform sampler2D _Ramp;

	// Custom lighting model that uses a texture ramp based
	// on angle between light direction and normal
	inline half4 LightingToonRamp (SurfaceOutput s, half3 lightDir, half atten)
	{
		#ifndef USING_DIRECTIONAL_LIGHT
		lightDir = normalize(lightDir);
		#endif
		// Wrapped lighting
		half d = dot (s.Normal, lightDir) * 0.5 + 0.5;
		// Applied through ramp
		half3 ramp = tex2D (_Ramp, float2(d,d)).rgb;
		half4 c;
		c.rgb = s.Albedo * _LightColor0.rgb * ramp * (atten * 2);
		c.a = 0;
		return c;
	}

	// Surface shader input structure
	struct Input {
		float2 uv_Control : TEXCOORD0;
		float2 uv_Splat0 : TEXCOORD1;
		float2 uv_Splat1 : TEXCOORD2;
		float2 uv_Splat2 : TEXCOORD3;
		float2 uv_Splat3 : TEXCOORD4;
	};

	// Surface Shader function
	void surf (Input IN, inout SurfaceOutput o) {
		fixed4 splat_control = tex2D (_Control, IN.uv_Control);
		fixed3 col;
		col  = splat_control.r * tex2D (_Splat0, IN.uv_Splat0).rgb;
		col += splat_control.g * tex2D (_Splat1, IN.uv_Splat1).rgb;
		col += splat_control.b * tex2D (_Splat2, IN.uv_Splat2).rgb;
		col += splat_control.a * tex2D (_Splat3, IN.uv_Splat3).rgb;
		o.Albedo = col * _Color;
		o.Alpha = 0.0;
	}
	ENDCG

	// Use the Outline Pass from the default Toon shader
	UsePass "Toon/Basic Outline/OUTLINE"

} // End SubShader

// Fallback to Diffuse
Fallback "Diffuse"

} // Ehd Shader

 

And here’s the output:

image

This entry was posted in Game Dev and tagged , , . Bookmark the permalink.

6 Responses to Custom Unity Terrain Material / Shaders

  1. Dagan says:

    Hi,
    Do you know how to change the terrain’s base material dynamically c# preferred?

    Thank you

  2. Vermacian says:

    Thank you!!!!!!!!

  3. bombjack3000 says:

    Thank you very much!
    You saved me hours of hacking!
    😀

  4. Ricardo says:

    Hello,

    I have a question regarding shaders for terrain, I am trying to get a terrain transparency (from here: http://wiki.unity3d.com/index.php/TerrainTransparency) but still applying a normal map and specularity but I am having problems, when I try to change the pragma from Lambert to BlinnPhong the specularity and normal map work but the alpha is not longer working.

    Do you know how to solve the issue?

    Thanks a lot

  5. Pingback: Terrible question about creating 'terrain' shader for custom mesh | Ceiba3D Studio

  6. Don says:

    Awesome tutorial – thanks.

    I actually will probably end up using something from your example code, for an in-game map – the sample outlines look really similar to a mock-up I did in Lightwave, so should work very nicely for a first-pass.

    Don
    have a great day

Leave a comment