Procedural Terrain Splatmapping

If you try searching for information on how to assign textures to Unity terrain through code (i.e. derive a “splatmap” of textures based on the terrain heightmap profile itself), you’ll probably end up being sent to this post on the Unity Answers site. Despite being over 3 years old, it still seems to be pretty much the only helpful demonstration of accessing Unity’s somewhat poorly-documented terrain functions through script.

However, while its a good starting point, the script there suffers from several shortcomings (I’m not blaming the original author – I imagine that at the time he wrote it, he probably didn’t expect it to become the authoritative source on the matter!):

  • It only works if your terrain’s splatmap has the same dimensions as its heightmap.
  • It allows for only three textures to be blended.
  • The y/x axes are inverted.
  • The method of normalisation is incorrect. ( Vector3.Normalize() sets the magnitude of the vector representing the texture weights to 1 – i.e. two equal textures will each have weight of 0.707. What instead is required is that the component weights should sum to 1 – i.e. each have weight of 0.5).

Attached below is my version of an automated splatmap creation script which attempts to correct some of the issues in that earlier code. It allows for any number of terrain textures to be blended based on the height, normal, steepness, or any other rules you create for each part of the terrain. Just attach the AssignSplatMap C# script below onto any terrain gameobject in the scene (having first assigned the appropriate textures to the terrain) and hit play.

using UnityEngine;
using System.Collections;
using System.Linq; // used for Sum of array

public class AssignSplatMap : MonoBehaviour {

	void Start () {
		// Get the attached terrain component
                Terrain terrain = GetComponent();
		
		// Get a reference to the terrain data
	        TerrainData terrainData = terrain.terrainData;

		// Splatmap data is stored internally as a 3d array of floats, so declare a new empty array ready for your custom splatmap data:
  		float[, ,] splatmapData = new float[terrainData.alphamapWidth, terrainData.alphamapHeight, terrainData.alphamapLayers];
		
	    for (int y = 0; y < terrainData.alphamapHeight; y++)
    	    {
    		for (int x = 0; x < terrainData.alphamapWidth; x++)
   			 {
				// Normalise x/y coordinates to range 0-1 
				float y_01 = (float)y/(float)terrainData.alphamapHeight;
				float x_01 = (float)x/(float)terrainData.alphamapWidth;
				
   				// Sample the height at this location (note GetHeight expects int coordinates corresponding to locations in the heightmap array)
    			        float height = terrainData.GetHeight(Mathf.RoundToInt(y_01 * terrainData.heightmapHeight),Mathf.RoundToInt(x_01 * terrainData.heightmapWidth) );
				
				// Calculate the normal of the terrain (note this is in normalised coordinates relative to the overall terrain dimensions)
				Vector3 normal = terrainData.GetInterpolatedNormal(y_01,x_01);
     
				// Calculate the steepness of the terrain
				float steepness = terrainData.GetSteepness(y_01,x_01);
				
				// Setup an array to record the mix of texture weights at this point
				float[] splatWeights = new float[terrainData.alphamapLayers];
				
				// CHANGE THE RULES BELOW TO SET THE WEIGHTS OF EACH TEXTURE ON WHATEVER RULES YOU WANT
	
				// Texture[0] has constant influence
				splatWeights[0] = 0.5f;
				
				// Texture[1] is stronger at lower altitudes
				splatWeights[1] = Mathf.Clamp01((terrainData.heightmapHeight - height));
				
				// Texture[2] stronger on flatter terrain
				// Note "steepness" is unbounded, so we "normalise" it by dividing by the extent of heightmap height and scale factor
				// Subtract result from 1.0 to give greater weighting to flat surfaces
				splatWeights[2] = 1.0f - Mathf.Clamp01(steepness*steepness/(terrainData.heightmapHeight/5.0f));
				
				// Texture[3] increases with height but only on surfaces facing positive Z axis 
				splatWeights[3] = height * Mathf.Clamp01(normal.z);
				
				// Sum of all textures weights must add to 1, so calculate normalization factor from sum of weights
				float z = splatWeights.Sum();
				
				// Loop through each terrain texture
				for(int i = 0; i<terrainData.alphamapLayers; i++){
					
					// Normalize so that sum of all texture weights = 1
					splatWeights[i] /= z;
					
					// Assign this point to the splatmap array
					splatmapData[x, y, i] = splatWeights[i];
				}
		    }
    	}
     
    	// Finally assign the new splatmap to the terrainData:
    	terrainData.SetAlphamaps(0, 0, splatmapData);
	}
}



 

Here’s some examples of rules you might want to implement for various textures:

image

Texture weight based on surface normal (useful for e.g. snow accumulating on one side of a mountain, moss growing on north side of a hill)

image

Texture weight based on height (e.g. ice caps at high altitudes, sand near sea-level)

image

Texture weight based on steepness (e.g. grass grows on relatively flat terrain)

 

Using this script with Unity’s standard terrain textures turns the following default grey terrain:

image

Into this slightly more attractive scene:

image

 

Sure there’s certainly a lot more you could do to improve the mapping, and it doesn’t compare to the output you get from professional world modelling tools such as World Machine. But, then again, it’s free, and it gives you a good start from which to refine further improvements to your terrain.

About these ads
This entry was posted in Game Dev, Spatial and tagged , , , . Bookmark the permalink.

5 Responses to Procedural Terrain Splatmapping

  1. Hi Alastair, I’m always interested in your articles, having just recently started getting into Unity myself also (from a spatial db / dev background). Do you happen to have a twitter account I could follow? Cheers!

  2. Pingback: Importing DEM Terrain Heightmaps for Unity using GDAL | Alastair Aitchison

  3. Excellent work! I’m actually using TerrainComposer, but if I’d seen this first, I might have considered doing this instead. I do have a question you may be able to help with. It seems like I can either have textures that look really great in first person but tiled off in the distance, or really great in a fly by, but pixelated in first person. Since I’m doing a racing game, I need to texture the terrain so that where we are looks great, but you can look off in the distance and it will still look great. Any ideas on how that works?

  4. TCGM says:

    How would one convert this correctly over to a runtime-able script? Instead of using already existing objects, my game instantiates and deletes tiles based on the player’s position during runtime. The TerrainData I instantiate from remains unchanged, however (it is still generated at the beginning of runtime though). I can probably go over it and figure it out, but I wanted to ask to save myself the trouble if you happened to already have a version like that.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s