Texture Terrain using Slope Angles

Terrain Toolkit has an excellent function for texturing a terrain based on setting heights where they exist and also blend between.

I have been unsuccessfully trying to convert this so that it textures a terrain based on the slope angle.

Is anyone interested in helping out to get this to work?

I have included an edited working version of both methods, my uJS converted method of texturing by heights from Terrain Toolkit, and my attempt for converting this to use the slope angle data rather than the heightmap data.

//------------------------------//
//  TerrainAlphamaps.js         //
//  Written by Alucard Jay      //
//  11/10/2013                  //
//------------------------------//

//  based on Terrain Toolkit ( TerrainToolkit.cs ) by sixtimesnothing 


#pragma strict


#if UNITY_EDITOR

@ContextMenu( "Generate AlphaMap" )

function GenerateAlphaMap() 
{
	Start();
}

#endif


//  Script Management
//	----------------------------------------------------------------------------


enum TextureMethod
{
	UsingHeights,
	UsingSlopeAngles
}

public var textureBy : TextureMethod;


function Start() 
{
	// Load or find the terrain and get Terrain Data
	GetTerrainData();
	
	// Texture the Terrain
	switch ( textureBy )
	{
		case TextureMethod.UsingHeights :
			TextureTerrainUsingHeights();
		break;
		
		case TextureMethod.UsingSlopeAngles :
			TextureTerrainUsingSlopeAngles();
		break;
	}
}


function Update() 
{
	if ( Input.GetMouseButtonDown(0) )
	{
		Start();
	}
}


//  Terrain Data
//	----------------------------------------------------------------------------


public var terrain : Terrain;
private var terrainData : TerrainData;

private var heightmapWidth : int;
private var heightmapHeight : int;


function GetTerrainData() 
{
	if ( !terrain )
	{
		terrain = Terrain.activeTerrain;
	}
	
	terrainData = terrain.terrainData;
	
	heightmapWidth = terrain.terrainData.heightmapWidth;
	heightmapHeight = terrain.terrainData.heightmapHeight;
}


//	============================================================================


//  ============================================================================
//  
//  
//  below is written based from sixtimesnothing Terrain Toolkit ( TerrainToolkit.cs )
//  
//  http://www.sixtimesnothing.com/terraintoolkit
//  
//  http://sixtimesnothing.wordpress.com/
//  
//  http://code.google.com/p/unityterraintoolkit/
//  
//
//	============================================================================
 

//  Terrain Texturing
//	----------------------------------------------------------------------------


// -- Original using Heights --


private var splatPrototypes : SplatPrototype[];

public var slopeBlendMinAngle : float = 32.0;
public var slopeBlendMaxAngle : float = 48.0;

public var heightBlendPoints : float[];


function TextureTerrainUsingHeights() 
{
	// store a reference to the terrain textures
	splatPrototypes = terrain.terrainData.splatPrototypes;
	
	
	// check there are 4 textures
	var nTextures : int = splatPrototypes.Length;
	
	if ( nTextures != 4 ) 
	{
		Debug.LogError( "Error: You must assign 4 textures." );
		return;
	}
	
	// check there are 4 values in the blend points arrays
	var nHeights : int = heightBlendPoints.Length;
	
	if ( nHeights != 4 ) 
	{
		Debug.LogError( "Error: You must assign 4 values to the Height BlendPoints array." );
		//return;
		
		// example of how arrays should be populated
		
		// heights are normalized values
		heightBlendPoints = new float[4];
		heightBlendPoints[0] = 0.2;
		heightBlendPoints[1] = 0.35;
		heightBlendPoints[2] = 0.5;
		heightBlendPoints[3] = 0.65;
	}
	
	// --
	
	
	var Tw : int = terrainData.heightmapWidth - 1;
	var Th : int = terrainData.heightmapHeight - 1;
	
	var heightMapTexData : float[,] = new float[Tw, Th];
	var slopeMapData : float[,] = new float[Tw, Th];
	var splatMapData : float[,,];
	
	terrainData.alphamapResolution = Tw;
	splatMapData = terrainData.GetAlphamaps(0, 0, Tw, Tw);
	
	
	// Angles to difference...
	var terSize : Vector3 = terrainData.size;
	var slopeBlendMinimum : float = ((terSize.x / Tw) * Mathf.Tan(slopeBlendMinAngle * Mathf.Deg2Rad)) / terSize.y;
	var slopeBlendMaximum : float = ((terSize.x / Tw) * Mathf.Tan(slopeBlendMaxAngle * Mathf.Deg2Rad)) / terSize.y;
	
	
	// ----
	
	var greatestHeight : float = 0.0;
	var xNeighbours : int;
	var yNeighbours : int;
	var xShift : int;
	var yShift : int;
	var xIndex : int;
	var yIndex : int;
	var heightMap : float[,] = terrainData.GetHeights( 0, 0, Tw, Th );
	
	var slopeAngles : float[,] = new float[ Tw, Th ];
	
	
	for (var Ty : int = 0; Ty < Th; Ty++) 
	{
		// y...
		if (Ty == 0) 
		{
			yNeighbours = 2;
			yShift = 0;
			yIndex = 0;
		} 
		else if (Ty == Th - 1) 
		{
			yNeighbours = 2;
			yShift = -1;
			yIndex = 1;
		} 
		else 
		{
			yNeighbours = 3;
			yShift = -1;
			yIndex = 1;
		}
		
		for (var Tx : int = 0; Tx < Tw; Tx++) 
		{
			// x...
			if (Tx == 0) 
			{
				xNeighbours = 2;
				xShift = 0;
				xIndex = 0;
			} 
			else if (Tx == Tw - 1) 
			{
				xNeighbours = 2;
				xShift = -1;
				xIndex = 1;
			} 
			else 
			{
				xNeighbours = 3;
				xShift = -1;
				xIndex = 1;
			}
			
			
			// ----
			
			
			// Get height...
			var h : float = heightMap[ Tx + xIndex + xShift, Ty + yIndex + yShift ];
			
			if (h > greatestHeight) 
			{
				greatestHeight = h;
				//Debug.Log( h );
			}
			
			// ...and apply to height map...
			heightMapTexData[Tx, Ty] = h;
			
			
			// ----
			
			
			// Calculate slope...
			var tCumulative : float = 0.0f;
			var nNeighbours : float = xNeighbours * yNeighbours - 1;
			var Ny : int;
			var Nx : int;
			var t : float;
			
			for (Ny = 0; Ny < yNeighbours; Ny++) 
			{
				for (Nx = 0; Nx < xNeighbours; Nx++) 
				{
					// Ignore the index...
					if (Nx != xIndex || Ny != yIndex) 
					{
						t = Mathf.Abs( h - heightMap[ Tx + Nx + xShift, Ty + Ny + yShift ] );
						tCumulative += t;
					}
				}
			}
			
			var tAverage : float = tCumulative / nNeighbours;
			
			// ...and apply to the slope map...
			slopeMapData[Tx, Ty] = tAverage;
		}
		
		// Show progress...
		// float completePoints = Ty * Th;
		// float totalPoints = Tw * Th;
		// float percentComplete = (completePoints / totalPoints) * 0.6f;
		// textureProgressDelegate("Procedural Terrain Texture", "Generating height and slope maps. Please wait.", percentComplete);
	}
	
	// Blend slope...
	var sBlended : float;
	var Px : int;
	var Py : int;
	
	for (Py = 0; Py < Th; Py++) 
	{
		for (Px = 0; Px < Tw; Px++) 
		{
			sBlended = slopeMapData[Px, Py];
			
			if (sBlended < slopeBlendMinimum) 
			{
				sBlended = 0;
			} 
			else if (sBlended < slopeBlendMaximum) 
			{
				sBlended = (sBlended - slopeBlendMinimum) / (slopeBlendMaximum - slopeBlendMinimum);
			} 
			else if (sBlended > slopeBlendMaximum) 
			{
				sBlended = 1;
			}
			
			slopeMapData[Px, Py] = sBlended;
			splatMapData[Px, Py, 0] = sBlended;
		}
	}
	
	// Blend height maps...
	for (var i : int = 1; i < nTextures; i++) 
	{
		for (Py = 0; Py < Th; Py++) 
		{
			for (Px = 0; Px < Tw; Px++) 
			{
				var hBlendInMinimum : float = 0;
				var hBlendInMaximum : float = 0;
				var hBlendOutMinimum : float = 1;
				var hBlendOutMaximum : float = 1;
				var hValue : float;
				var hBlended : float = 0;
				
				// --
				
				// using heights
				
				if (i > 1) 
				{
					hBlendInMinimum = parseFloat( heightBlendPoints[i * 2 - 4] );
					hBlendInMaximum = parseFloat( heightBlendPoints[i * 2 - 3] );
				}
				
				if (i < nTextures - 1) 
				{
					hBlendOutMinimum = parseFloat( heightBlendPoints[i * 2 - 2] );
					hBlendOutMaximum = parseFloat( heightBlendPoints[i * 2 - 1] );
				}
				
				hValue = heightMapTexData[ Px, Py ];
				
				// --
				
				if (hValue >= hBlendInMaximum  hValue <= hBlendOutMinimum) 
				{
					// Full...
					hBlended = 1;
				} 
				else if (hValue >= hBlendInMinimum  hValue < hBlendInMaximum) 
				{
					// Blend in...
					hBlended = (hValue - hBlendInMinimum) / (hBlendInMaximum - hBlendInMinimum);
				} 
				else if (hValue > hBlendOutMinimum  hValue <= hBlendOutMaximum) 
				{
					// Blend out...
					hBlended = 1 - ((hValue - hBlendOutMinimum) / (hBlendOutMaximum - hBlendOutMinimum));
				}
				
				// --
				
				// Subtract slope...
				var sValue : float = slopeMapData[ Px, Py ];
				hBlended -= sValue;
				
				if (hBlended < 0) 
				{
					hBlended = 0;
				}
				
				splatMapData[ Px, Py, i ] = hBlended;
				
				// --
			}
		}
	}
	
	
	// Generate splat maps...
	terrainData.SetAlphamaps( 0, 0, splatMapData );
	
	
	// Clean up...
	heightMapTexData = null;
	slopeMapData = null;
	splatMapData = null;
	
	// ----
	
	Debug.Log( "Splatmap Updated using Heights" );
}


//	============================================================================


// -- Modified using Slope Angles --


public var slopeBlendPoints : float[];


function TextureTerrainUsingSlopeAngles() 
{
	// store a reference to the terrain textures
	splatPrototypes = terrain.terrainData.splatPrototypes;
	
	
	// check there are 4 textures
	var nTextures : int = splatPrototypes.Length;
	
	if ( nTextures != 4 ) 
	{
		Debug.LogError( "Error: You must assign 4 textures." );
		return;
	}
	
	// check there are 4 values in the blend points arrays
	var nSlopes : int = slopeBlendPoints.Length;
	
	if ( nSlopes != 4 ) 
	{
		Debug.LogError( "Error: You must assign 4 values to the Slope BlendPoints array." );
		//return;
		
		// example of how arrays should be populated
		
		// slopes are angles in degrees
		slopeBlendPoints = new float[4];
		slopeBlendPoints[0] = 10.0;
		slopeBlendPoints[1] = 15.0;
		slopeBlendPoints[2] = 27.0;
		slopeBlendPoints[3] = 32.0;
	}
	
	// --
	
	
	var Tw : int = terrainData.heightmapWidth - 1;
	var Th : int = terrainData.heightmapHeight - 1;
	
	var heightMapTexData : float[,] = new float[Tw, Th];
	var slopeMapData : float[,] = new float[Tw, Th];
	var splatMapData : float[,,];
	
	terrainData.alphamapResolution = Tw;
	splatMapData = terrainData.GetAlphamaps(0, 0, Tw, Tw);
	
	
	// Angles to difference...
	var terSize : Vector3 = terrainData.size;
	var slopeBlendMinimum : float = ((terSize.x / Tw) * Mathf.Tan(slopeBlendMinAngle * Mathf.Deg2Rad)) / terSize.y;
	var slopeBlendMaximum : float = ((terSize.x / Tw) * Mathf.Tan(slopeBlendMaxAngle * Mathf.Deg2Rad)) / terSize.y;
	
	
	// ----
	
	var greatestHeight : float = 0.0;
	var xNeighbours : int;
	var yNeighbours : int;
	var xShift : int;
	var yShift : int;
	var xIndex : int;
	var yIndex : int;
	var heightMap : float[,] = terrainData.GetHeights( 0, 0, Tw, Th );
	
	var slopeAngles : float[,] = new float[ Tw, Th ];
	
	
	for (var Ty : int = 0; Ty < Th; Ty++) 
	{
		// y...
		if (Ty == 0) 
		{
			yNeighbours = 2;
			yShift = 0;
			yIndex = 0;
		} 
		else if (Ty == Th - 1) 
		{
			yNeighbours = 2;
			yShift = -1;
			yIndex = 1;
		} 
		else 
		{
			yNeighbours = 3;
			yShift = -1;
			yIndex = 1;
		}
		
		for (var Tx : int = 0; Tx < Tw; Tx++) 
		{
			// x...
			if (Tx == 0) 
			{
				xNeighbours = 2;
				xShift = 0;
				xIndex = 0;
			} 
			else if (Tx == Tw - 1) 
			{
				xNeighbours = 2;
				xShift = -1;
				xIndex = 1;
			} 
			else 
			{
				xNeighbours = 3;
				xShift = -1;
				xIndex = 1;
			}
			
			
			// ----
			
			
			// Get height...
			var h : float = heightMap[ Tx + xIndex + xShift, Ty + yIndex + yShift ];
			
			if (h > greatestHeight) 
			{
				greatestHeight = h;
				//Debug.Log( h );
			}
			
			// ...and apply to height map...
			heightMapTexData[Tx, Ty] = h;
			
			
			// --
			
			// get slope angle instead
			
			var slopeX : float = ( Tx + xIndex + xShift );
			var slopeY : float = ( Ty + yIndex + yShift );
			
			var readPos : Vector2 = new Vector2( slopeX / Tx, slopeY / Ty );
			
			slopeAngles[ Tx, Ty ] = terrainData.GetSteepness( readPos.y, readPos.x ); // y, x : reversed as heightmap is reversed
			
			
			// ----
			
			
			// Calculate slope...
			var tCumulative : float = 0.0f;
			var nNeighbours : float = xNeighbours * yNeighbours - 1;
			var Ny : int;
			var Nx : int;
			var t : float;
			
			for (Ny = 0; Ny < yNeighbours; Ny++) 
			{
				for (Nx = 0; Nx < xNeighbours; Nx++) 
				{
					// Ignore the index...
					if (Nx != xIndex || Ny != yIndex) 
					{
						t = Mathf.Abs( h - heightMap[ Tx + Nx + xShift, Ty + Ny + yShift ] );
						tCumulative += t;
					}
				}
			}
			
			var tAverage : float = tCumulative / nNeighbours;
			
			// ...and apply to the slope map...
			slopeMapData[Tx, Ty] = tAverage;
		}
		
		// Show progress...
		// float completePoints = Ty * Th;
		// float totalPoints = Tw * Th;
		// float percentComplete = (completePoints / totalPoints) * 0.6f;
		// textureProgressDelegate("Procedural Terrain Texture", "Generating height and slope maps. Please wait.", percentComplete);
	}
	
	// Blend slope...
	var sBlended : float;
	var Px : int;
	var Py : int;
	
	for (Py = 0; Py < Th; Py++) 
	{
		for (Px = 0; Px < Tw; Px++) 
		{
			sBlended = slopeMapData[Px, Py];
			
			if (sBlended < slopeBlendMinimum) 
			{
				sBlended = 0;
			} 
			else if (sBlended < slopeBlendMaximum) 
			{
				sBlended = (sBlended - slopeBlendMinimum) / (slopeBlendMaximum - slopeBlendMinimum);
			} 
			else if (sBlended > slopeBlendMaximum) 
			{
				sBlended = 1;
			}
			
			slopeMapData[Px, Py] = sBlended;
			splatMapData[Px, Py, 0] = sBlended;
		}
	}
	
	// Blend height maps...
	for (var i : int = 1; i < nTextures; i++) 
	{
		for (Py = 0; Py < Th; Py++) 
		{
			for (Px = 0; Px < Tw; Px++) 
			{
				var hBlendInMinimum : float = 0;
				var hBlendInMaximum : float = 0;
				var hBlendOutMinimum : float = 1;
				var hBlendOutMaximum : float = 1;
				var hValue : float;
				var hBlended : float = 0;
				
				// --
				
				/*
				
				// using heights
				
				if (i > 1) 
				{
					hBlendInMinimum = parseFloat( heightBlendPoints[i * 2 - 4] );
					hBlendInMaximum = parseFloat( heightBlendPoints[i * 2 - 3] );
				}
				
				if (i < nTextures - 1) 
				{
					hBlendOutMinimum = parseFloat( heightBlendPoints[i * 2 - 2] );
					hBlendOutMaximum = parseFloat( heightBlendPoints[i * 2 - 1] );
				}
				
				*/
				
				// --
				
				// using slope angles
				
				if (i > 1) 
				{
					hBlendInMinimum = parseFloat( slopeBlendPoints[i * 2 - 4] );
					hBlendInMaximum = parseFloat( slopeBlendPoints[i * 2 - 3] );
				}
				
				if (i < nTextures - 1) 
				{
					hBlendOutMinimum = parseFloat( slopeBlendPoints[i * 2 - 2] );
					hBlendOutMaximum = parseFloat( slopeBlendPoints[i * 2 - 1] );
				}
				
				
				// --
				
				//using heights
				//hValue = heightMapTexData[ Px, Py ];
				
				// now using slope angles
				hValue = slopeAngles[ Px, Py ];
				
				// normalize all angle values to 90 degrees
				var maxAngle : float = 90.0;
				
				hBlendInMinimum /= maxAngle;
				hBlendInMaximum /= maxAngle;
				hBlendOutMinimum /= maxAngle;
				hBlendOutMaximum /= maxAngle;
				
				hValue /= maxAngle;
				
				
				// --
				
				
				if (hValue >= hBlendInMaximum  hValue <= hBlendOutMinimum) 
				{
					// Full...
					hBlended = 1;
				} 
				else if (hValue >= hBlendInMinimum  hValue < hBlendInMaximum) 
				{
					// Blend in...
					hBlended = (hValue - hBlendInMinimum) / (hBlendInMaximum - hBlendInMinimum);
				} 
				else if (hValue > hBlendOutMinimum  hValue <= hBlendOutMaximum) 
				{
					// Blend out...
					hBlended = 1 - ((hValue - hBlendOutMinimum) / (hBlendOutMaximum - hBlendOutMinimum));
				}
				
				// --
				
				// Subtract slope...
				var sValue : float = slopeMapData[ Px, Py ];
				hBlended -= sValue;
				
				if (hBlended < 0) 
				{
					hBlended = 0;
				}
				
				splatMapData[ Px, Py, i ] = hBlended;
				
				// --
			}
		}
	}
	
	
	// Generate splat maps...
	terrainData.SetAlphamaps( 0, 0, splatMapData );
	
	
	// Clean up...
	slopeMapData = null;
	splatMapData = null;
	slopeAngles = null;
	
	// ----
	
	Debug.Log( "Splatmap Updated using Slope Angles" );
}


// =================================================================================

I posted this question as I have spent weeks trying to write my own method of blending textures between heights.

I was only setting absolute texture values based on simple conditionals :

			// --
			
			var terrTex : int = 0;
			
			if ( steepness < slope1 )
			{
				terrTex = 1;
			}
			else if ( steepness < slope2 )
			{
				terrTex = 2;
			}
			else if ( steepness < slope3 )
			{
				terrTex = 3;
			}
			
			// --
			
			switch( terrTex ) 
			{
				case 1 : 
					alphas[ w, h, 0 ] = 0;
					alphas[ w, h, 1 ] = 1;
					alphas[ w, h, 2 ] = 0;
					alphas[ w, h, 3 ] = 0;
				break;
				
				case 2 : 
					alphas[ w, h, 0 ] = 0;
					alphas[ w, h, 1 ] = 0;
					alphas[ w, h, 2 ] = 1;
					alphas[ w, h, 3 ] = 0;
				break;
				
				case 3 : 
					alphas[ w, h, 0 ] = 0;
					alphas[ w, h, 1 ] = 0;
					alphas[ w, h, 2 ] = 0;
					alphas[ w, h, 3 ] = 1;
				break;
				
				default : 
					alphas[ w, h, 0 ] = 1;
					alphas[ w, h, 1 ] = 0;
					alphas[ w, h, 2 ] = 0;
					alphas[ w, h, 3 ] = 0;
				break;
			}
			
			// --

After struggling with how to blend inbetween these absolute values, I looked at the TerrainToolkit and thought there may be a way to adapt their script (as illustrated in my questions’ code).

However I just went back to my original script, and the solution was so simple. I just had to add conditions for when the slope angle was between textures, and calculate alphas based on those differences.

			// --
			
			
			var cond : int = 0;
			
			if ( steepness <= slopeAngles[ 1 ] )
			{
				cond = 1;
			}
			else if ( steepness > slopeAngles[ 1 ]  steepness <= slopeAngles[ 2 ] )
			{
				cond = 2;
			}
			else if ( steepness > slopeAngles[ 2 ]  steepness <= slopeAngles[ 3 ] )
			{
				cond = 3;
			}
			else if ( steepness > slopeAngles[ 3 ]  steepness <= slopeAngles[ 4 ] )
			{
				cond = 4;
			}
			else if ( steepness > slopeAngles[ 4 ]  steepness <= slopeAngles[ 5 ] )
			{
				cond = 5;
			}
			else if ( steepness > slopeAngles[ 5 ]  steepness <= slopeAngles[ 6 ] )
			{
				cond = 6;
			}
			
			
			// --
			
			
			var curr : float;
			var max : float;
			
			var alpha1 : float;
			var alpha2 : float;
			
			
			switch( cond ) 
			{
				case 1 : 
					alphas[ w, h, 0 ] = 0;
					alphas[ w, h, 1 ] = 1;
					alphas[ w, h, 2 ] = 0;
					alphas[ w, h, 3 ] = 0;
				break;
				
				case 2 : 
					curr = steepness - slopeAngles[ 1 ];
					max = slopeAngles[ 2 ] - slopeAngles[ 1 ];
					
					alpha1 = curr / max;
					alpha2 = 1.0 - alpha1;
					
					alphas[ w, h, 0 ] = 0;
					alphas[ w, h, 1 ] = alpha2;
					alphas[ w, h, 2 ] = alpha1;
					alphas[ w, h, 3 ] = 0;
				break;
				
				case 3 : 
					alphas[ w, h, 0 ] = 0;
					alphas[ w, h, 1 ] = 0;
					alphas[ w, h, 2 ] = 1;
					alphas[ w, h, 3 ] = 0;
				break;
				
				case 4 : 
					curr = steepness - slopeAngles[ 3 ];
					max = slopeAngles[ 4 ] - slopeAngles[ 3 ];
					
					alpha1 = curr / max;
					alpha2 = 1.0 - alpha1;
					
					alphas[ w, h, 0 ] = 0;
					alphas[ w, h, 1 ] = 0;
					alphas[ w, h, 2 ] = alpha2;
					alphas[ w, h, 3 ] = alpha1;
				break;
				
				case 5 : 
					alphas[ w, h, 0 ] = 0;
					alphas[ w, h, 1 ] = 0;
					alphas[ w, h, 2 ] = 0;
					alphas[ w, h, 3 ] = 1;
				break;
				
				case 6 : 
					curr = steepness - slopeAngles[ 5 ];
					max = slopeAngles[ 6 ] - slopeAngles[ 5 ];
					
					alpha1 = curr / max;
					alpha2 = 1.0 - alpha1;
					
					alphas[ w, h, 0 ] = alpha1;
					alphas[ w, h, 1 ] = 0;
					alphas[ w, h, 2 ] = 0;
					alphas[ w, h, 3 ] = alpha2;
				break;
				
				default : 
					alphas[ w, h, 0 ] = 1;
					alphas[ w, h, 1 ] = 0;
					alphas[ w, h, 2 ] = 0;
					alphas[ w, h, 3 ] = 0;
				break;
			}
			
			// --

Problem solved, terrain is now painted and blending based on slope angles !

=]