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.

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

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

  5. Senitelty says:

    I got an error when saved script says:
    Assets/_Terrains/AssignSplatMap.cs(9,35): error CS0411: The type arguments for method `UnityEngine.Component.GetComponent()’ cannot be inferred from the usage. Try specifying the type arguments explicitly

  6. MrBungle001 says:

    I was wondering if someone could help me. I am trying to paint the terrain at runtime only inside the bounds of an object. Basically I have a script that allows you to place a building and I would like to paint dirt around the perimeter of the building. I am guess to do so would require me to get the Vector3 position of each alphamap co-ordinate and check to see if it in the bounds of the buildings collider. However I am having a bit of trouble figuring this out. This also might be a bit slow and was wondering if anyone had a more elegant solution.

    thanks in advance

  7. Tim says:

    Hi there,

    Really like the script but I’m struggling to understand how to use it in the way I want to. I require 1 texture (sand) to be displayed at low grounds, such as riverbeds. Grass to be displayed in most other places and then rocks to be display on steep surfaces. Any help you could give would be amazing!

  8. EA says:

    Hey there.

    First I want to say, thanks for creating something like this. It’s really cool and is way more organic that anything I could have created easily. This is a valuable script/tool.

    That said, I’m having an issue with something. I’m trying to make certain terrains prevalent at lower/higher altitudes. However, when I use your code for that:

    splatWeights[0] = Mathf.Clamp01((terrainData.heightmapHeight – height));
    (and ) conversely for higher altitudes
    splatWeights[0] = Mathf.Clamp01((terrainData.heightmapHeight + height));

    I get nothing for the lower altitudes, and everything is overlayed with the higher altitudes one. I’m pretty sure this is related to the height variable (because it’s the only difference). And it makes me think that it’s taking the height value at the very bottom of the terrain or something.

    When I take a look at the height variable, I see that your code uses terrainData.GetHeight, along with your modified coordinates for x and y. I figured I could change the x and y coordinates to find a place on my terrain to get a specific height I wanted for the terrain, to specific integers like (4400, 3245)–but this didn’t work. I still get an overlay of the texture over everything.

    So I guess I have two questions. One, can you explain your formula a little bit (the y_01 and x_01, combined with the heightmap width/height), so I can change the height variable to different heights?

    Second, as an alternative, do you know if it’s possible to get specific heights? I could use this instead of adjusting the height variable, it might be a simpler version. I don’t know.

    Either way, if you could help me it would be great. Thanks.

  9. patbertoldo says:

    Hey Alistair,

    Thanks for sharing this, it works great and the commenting in the code is spot on.
    Using Unity’s Perlin Noise sample, I’ve created a script that will generate a random height map, apply it to the terrain and now with your script it goes over the terrain with the textures. Simply fantastic.

  10. Jesús Jaraíz says:

    Great post and great work! I have built a procedural terrain generator for Unity and this script enhances the texture results a lot. Thanks!

  11. Pingback: Adding terrain textures procedurally | TRIFORCE STUDIO

  12. This is a great script. I’ve been having a play. Do you have any extra examples of how one may define the rules for texture blending? For example, only paint a sandy texture on flat parts below sea level. Or a paint cliff texture only on steep gradients. I have no clue looking at the script the logic involved in defining the rules. I am eager to learn in order to unlock the power of using the terrain data.

Leave a comment