Turn a Terrain into a Contoured Map
Is there a way to create a height-contoured map from a Unity Terrain other than to manually paint over it? Something like Arma to this effect:
I've tried using an exported heightmap but Photoshop-fu can only take one so far.
Answer by Dave29483 · Sep 11, 2014 at 07:49 AM
Updated Post
Figured I may aswell try it in Unity... (Output shown on plane. Uses alpha channel so lines can be overlaid)
Code below image.
Listing for HeightmapToContourMap.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System;
public class ContourMap
{
//Creates contour map from Raw 16bpp heightmap data as Texture2D
//Returns null on failure
//If neither width nor height specified, then it's POT size will be guessed
public static Texture2D FromRawHeightmap16bpp( string fileName, int width = 0, int height = 0 )
{
if ( !File.Exists( fileName ) )
{
Debug.Log( "Heightmap not found " + fileName );
return null;
}
//dimensions
int _width = width;
int _height = height;
Color32 bandColor = new Color32(255, 255, 255, 255);
Color32 bkgColor = new Color32(0, 0, 0, 0);
//Output
Texture2D topoMap;
//Read raw 16bit heightmap
byte[] rawBytes = System.IO.File.ReadAllBytes( fileName );
short[] rawImage = new short[rawBytes.Length / 2];
//Create slice buffer
bool[] slice = new bool[rawImage.Length];
//Convert to bytes to short
Buffer.BlockCopy( rawBytes, 0, rawImage, 0, rawBytes.Length );
//Create Texture2D with estimated or specified width
if ( _width == 0 || _height == 0 )
{
_width = (int)Math.Sqrt( rawImage.Length ); //Estimated width/height
_height = _width;
topoMap = new Texture2D( _width, _height );
}
else
{
topoMap = new Texture2D( _width, _height );
}
topoMap.anisoLevel = 16;
//Set background
for (int x = 0; x < _width; x++)
{
for (int y = 0; y < _height; y++)
{
topoMap.SetPixel( x, y, bkgColor );
}
}
//Initial Min/Max values for signed 16bit value
int minHeight = 32767;
int maxHeight = -32767;
//Find lowest and highest points
for ( int i = 0; i < rawImage.Length; i++ )
{
if ( rawImage[ i ] < minHeight )
{
minHeight = rawImage[ i ];
}
if ( rawImage[ i ] > maxHeight )
{
maxHeight = rawImage[ i ];
}
}
Debug.Log("Min: " + minHeight.ToString() + ", Max: " + maxHeight.ToString());
//Create height band list
int bandDistance = maxHeight / 12; //Number of height bands to create
List<int> bands = new List<int>();
//Get ranges
int r = minHeight + bandDistance;
while ( r < maxHeight )
{
bands.Add( r );
r += bandDistance;
}
//Draw bands
for ( int b = 0; b < bands.Count; b++ )
{
//Get Slice
for ( int i = 0; i < rawImage.Length; i++ )
{
if ( rawImage[ i ] >= bands[ b ] )
{
slice[ i ] = true;
}
else
{
slice[ i ] = false;
}
}
//Detect edges on slice and write to output
for ( int y = 1; y < _height - 1; y++ )
{
for ( int x = 1; x < _width - 1; x++ )
{
if ( slice[ y * _width + x ] == true )
{
if ( slice[ y * _width + ( x - 1 ) ] == false || slice[ y * _width + ( x + 1 ) ] == false || slice[ ( y - 1 ) * _width + x ] == false || slice[ ( y + 1 ) * _width + x ] == false )
{
topoMap.SetPixel( x, y, bandColor );
}
}
}
}
}
topoMap.Apply();
//Return result
return topoMap;
}
}
Listing for TopoLines.cs
using UnityEngine;
using System.Collections;
public class TopoLines : MonoBehaviour
{
public string heightmapPath = "/Users/dave/desktop/terrain.raw";
public Texture2D topoMap;
public Material outputMaterial;
void Start()
{
topoMap = ContourMap.FromRawHeightmap16bpp(heightmapPath);
if (topoMap == null)
{
Debug.Log("Creation of topomap failed.");
}
else
{
Debug.Log("Creation of topomap was successful.");
}
if (outputMaterial != null)
{
outputMaterial.mainTexture = topoMap;
}
}
}
How would this be implemented? While building the level? During runtime?
Also regarding drawing the overlay, were you referring to a graphics program? or can unity edit images through scripting? Sorry about all the questions and thanks for the reply.
Interesting. I'll see if I can get something similar working for Unity and check back here. $$anonymous$$uch appreciated.
Had a bit of free time so did another example which uses 16bit raw height map data which is a more likely scenario. It is also considerably faster.
Figured it out...
After creating Texture2D clear background;
//Set background
for (int x = 0; x < _width; x++)
{
for (int y = 0; y < _height; y++)
{
topo$$anonymous$$ap.SetPixel( x, y, bkgColor );
}
}
Then remove the else section from edge detection so it looks like so;
if ( slice[ y * _width + ( x - 1 ) ] == false || slice[ y * _width + ( x + 1 ) ] == false || slice[ ( y - 1 ) * _width + x ] == false || slice[ ( y + 1 ) * _width + x ] == false )
{
topo$$anonymous$$ap.SetPixel( x, y, bandColor );
}
I also updated code listing above with fix and removed the unnecessary slice allocation in the band loop. Same slice buffer can be reused.
Answer by AlucardJay · Mar 25, 2016 at 05:05 PM
This is a modified script from the excellent answer by Dave29483 for creating a contour map texture from a terrain. Add the function to that class, or just use the class as below.
Call with :
ContourMap.FromTerrain( terrain ) // use default number of bands and colours
ContourMap.FromTerrain( terrain, numberOfBands ) // assign number of bands, use default colours
ContourMap.FromTerrain( terrain, numberOfBands, bandColor, bkgColor ) // assign number of bands and colours
modified ContourMap.cs :
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class ContourMap : MonoBehaviour
{
// Creates contour map from terrain heightmap data as Texture2D
// use default colours and optional parameter numberOfBands
public static Texture2D FromTerrain( Terrain terrain, int numberOfBands = 12 )
{
return FromTerrain( terrain, numberOfBands, Color.white, Color.clear );
}
// define all parameters
public static Texture2D FromTerrain( Terrain terrain, int numberOfBands, Color bandColor, Color bkgColor )
{
// dimensions
int width = terrain.terrainData.heightmapWidth;
int height = terrain.terrainData.heightmapHeight;
// heightmap data
float[,] heightmap = terrain.terrainData.GetHeights( 0, 0, width, height );
// Create Output Texture2D with heightmap dimensions
Texture2D topoMap = new Texture2D( width, height );
topoMap.anisoLevel = 16;
// array for storing colours to be applied to texture
Color[] colourArray = new Color[ width * height ];
// Set background
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
colourArray[ (y * width) + x ] = bkgColor;
}
}
// Initial Min/Max values for normalized terrain heightmap values
float minHeight = 1f;
float maxHeight = 0;
// Find lowest and highest points
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
if ( minHeight > heightmap[ y, x ] )
{
minHeight = heightmap[ y, x ];
}
if ( maxHeight < heightmap[ y, x ] )
{
maxHeight = heightmap[ y, x ];
}
}
}
// Create height band list
float bandDistance = ( maxHeight - minHeight ) / (float)numberOfBands; // Number of height bands to create
List< float > bands = new List< float >();
// Get ranges
float r = minHeight + bandDistance;
while ( r < maxHeight )
{
bands.Add( r );
r += bandDistance;
}
// Create slice buffer
bool[,] slice = new bool[ width, height ];
// Draw bands
for ( int b = 0; b < bands.Count; b++ )
{
// Get Slice
for ( int y = 0; y < height; y++ )
{
for ( int x = 0; x < width; x++ )
{
if ( heightmap[ y, x ] >= bands[ b ] )
{
slice[ x, y ] = true;
}
else
{
slice[ x, y ] = false;
}
}
}
// Detect edges on slice and write to output
for ( int y = 1; y < height - 1; y++ )
{
for ( int x = 1; x < width - 1; x++ )
{
if ( slice[ x, y ] == true )
{
if (
slice[ x - 1, y ] == false ||
slice[ x + 1, y ] == false ||
slice[ x, y - 1 ] == false ||
slice[ x, y + 1 ] == false )
{
// heightmap is read y,x from bottom left
// texture is read x,y from top left
// magic equation to find correct array index
int ind = ( ( height - y - 1 ) * width ) + ( width - x - 1 );
colourArray[ ind ] = bandColor;
}
}
}
}
}
// apply colour array to texture
topoMap.SetPixels( colourArray );
topoMap.Apply();
// Return result
return topoMap;
}
}
example TopoLines.cs :
using UnityEngine;
using System.Collections;
public class TopoLines : MonoBehaviour
{
public Terrain terrain;
public int numberOfBands = 12;
public Color bandColor = Color.white;
public Color bkgColor = Color.clear;
public Renderer outputPlain;
public Texture2D topoMap;
void Start()
{
GenerateTopoLines();
}
void Update()
{
if ( Input.GetMouseButtonDown(0) )
{
GenerateTopoLines();
}
}
void GenerateTopoLines()
{
//topoMap = ContourMap.FromTerrain( terrain );
//topoMap = ContourMap.FromTerrain( terrain, numberOfBands );
topoMap = ContourMap.FromTerrain( terrain, numberOfBands, bandColor, bkgColor );
if ( outputPlain )
{
outputPlain.material.mainTexture = topoMap;
}
}
}
Image from test. Change values in runtime and left-mouse in scene to observe :
Thanks a lot !!
This end with weird odd texture resolution but it works like a charm !!
The odd texture resolution is because Unity terrain heightmaps are 2^n + 1 (eg 257, 513, 1025), more info : http://docs.unity3d.com/$$anonymous$$anual/terrain-OtherSettings.html (scroll down to Resolution)
Heightmap Resolution : Pixel resolution of the terrain’s heightmap (should be a power of two plus one, eg, 513 = 512 + 1).
I chose to leave it, to return a texture with the true full range of heights. However if you wish the texture to be 2^n (256, 512, 1024) then the lines that set the width and height can be modified like this :
// dimensions
int width = terrain.terrainData.heightmapWidth - 1;
int height = terrain.terrainData.heightmapHeight - 1;
Unity is strange in how it uses different coordinate systems (screen, texture, heightmap all have different origins and axis?!!). I had some trouble myself with this while converting that script. You see the comment "// magic equation to find correct array index" ;)
All I can suggest is some trial and error to find the right index calculation. If both axis are flipped, try :
int ind = (y * width) + x;
or other combinations thereof :
int ind = ((height - y - 1) * width) + x; // flip y, keep x
int ind = (y * width) + (width - x - 1); // keep y, flip x
Answer by iconeo · Nov 10, 2016 at 07:37 PM
I am very new to unity but would like to implement this. Is there somewhere where I can get more instruction or even a sample project with this already setup?
Thanks.
Answer by Circool · Mar 02, 2018 at 08:33 AM
I've made an asset "Wire Terrain" and it can create mesh contours from terrain: https://assetstore.unity.com/packages/tools/modeling/wireterrain-94895
Your answer
Follow this Question
Related Questions
How do I create a terrain with an asset? 0 Answers
How can I edit my terrain around a street curb? 1 Answer
I have trees prefabs, how to make sure that they can be set on the terrain 0 Answers
Terrain Tree Index Removed, But Sometimes Not And I got An Error 0 Answers
Updating the terrain collider after importing raw data 4 Answers