- Home /
Just How Malleable Are Terrains, Really?
I'm currently working on the early stages of a project that will depend heavily on being able to modify the terrain. Altering heightmaps, altering textures, adding/removing mesh objects, associating arbitrary attributes with a terrain, and so forth. Oh, and all of these will need to be persistable. Also, being able to generate terrains, and package them up for dynamic loading, server-side. Plus I'd like a pony :-)
Now I know the only documented (and thus "official") aspect of terrains that can be changed at runtime is the heightmap. What else is there that's undocumented and Unity is willing to let people know about?
If it's not feasible to do what I want with existing terrains, I may have to resort to using mesh objects to do what I'd like. Ideally I'd like to have a completely new class of object - say, DynamicTerrain - that supported the abilities I need without having to compromise the current Terrain class. Now I know what that would involve, but I've got 20 years of C/C++ experience, nothing else on my plate (I've got a defense background but I'm stuck in a state that's got no defense work in it), and would be happy to give any resulting implementation of dynamic terrains to Unity - in addition to the project I'm currently trying to get going, having a more malleable terrain available would open up the possibility of several additional projects for me (of course, then I'd also need a fisheye lens camera option, but that's another question for another time...)
Pie in the sky, I know, but I thought it wouldn't hurt to at least ask.
Al
Answer by Jaap Kreijkamp · Jan 05, 2010 at 04:13 AM
I would say the pony is the easiest requirement.
The terrain asset is quite limited in what you can do with it in runtime. However... there are some undocumented API's you can use to basically construct/modify anything in a terrain. A simple google gave me the following code, don't know where it's from but it seems to use about anything terrain related to construct one dynamically. As the TerrainData API is undocumented it's unsupported and not guaranteed to work now or in the future...
I don't know who's the owner of the code below and haven't tried it myself but should be helpful to see how you can use the API.
Good luck with your project and pony.
//Download terrain assets referenced in headers i = 1; for(var v : String in trnArr) { //v = "name;r:width,height,length,heightmapResolution //,detailResolution,controlResolution,textureResolution;h:heightMapUrl"; var vS2 : String[] = v.Split(";"[0]); var tName = vS2[0]; for(i2 = 1; i2<vS2.length; i2++) { var str : String[] = vS2[i2].Split(":"[0]); if(str[0] == "r") var tRes : String[] = str[1].Split(","[0]); else if(str[0] == "h") var tHtmp : String = whirld.getURL(str[1]); else if(str[0] == "l") var tLtmp : String = whirld.getURL(str[1]); else if(str[0] == "s") var tSpmp : String = whirld.getURL(str[1]); else if(str[0] == "s2") var tSpmp2 : String = whirld.getURL(str[1]); else if(str[0] == "t") var tTxts : String[] = str[1].Split(","[0]); else if(str[0] == "d") var tDtmp : String = whirld.getURL(str[1]); } whirld.statusTxt = "Downloading Terrain " + i + " of " + trnArr.length + " (" + tName + "): " + tHtmp; www = new WWW(tHtmp); while(!www.isDone) { whirld.progress = www.progress; yield new WaitForSeconds(.1); } if (www.error != null) whirld.info += "\nTerrain Undownloadable: " + tName + " " + tHtmp + " (" + www.error + ")"; else { whirld.statusTxt = "Initializing " + tName + "..."; whirld.progress = 0; if(Application.isPlaying) yield;
var tWidth : int = parseInt(tRes[0]);
var tHeight : int = parseInt(tRes[1]);
var tLength : int = parseInt(tRes[2]);
var tHRes : int = parseInt(tRes[3]);
//var tDRes : int = parseInt(tRes[4]);
//var tCRes : int = parseInt(tRes[5]);
//var tBRes : int = parseInt(tRes[6]);
var trnDat : TerrainData = new TerrainData();
//Heights
trnDat.heightmapResolution = tHRes;
//trnDat.Init(tCRes, tDRes, tBRes);
var hmap = trnDat.GetHeights(0, 0, tHRes, tHRes);
var br : BinaryReader;
if(true) { //Terrain RAW file is compressed
/*var stream = new DeflateStream(new MemoryStream(www.bytes), CompressionMode.Decompress);
var buffer : byte[] = new byte[4096];
var ms : MemoryStream = new MemoryStream();
var bytesRead : int = 0;
while (bytesRead > 0) {
bytesRead = stream.Read(buffer, 0, buffer.Length);
if (bytesRead > 0) ms.Write(buffer, 0, bytesRead);
}
br = new BinaryReader(ms);*/
br = new BinaryReader(new MemoryStream(new Ionic.Zlib.GZipStream(new MemoryStream(), Ionic.Zlib.CompressionMode.Decompress).UncompressBuffer(www.bytes)));
}
else br = new BinaryReader(new MemoryStream(www.bytes));
for (var x : int = 0; x < tHRes; x++) for (var y : int = 0; y < tHRes; y++) hmap[x, y] = br.ReadUInt16() / 65535.00000000;
trnDat.SetHeights(0, 0, hmap);
trnDat.size = Vector3(tWidth, tHeight, tLength);
//Textures
if(tTxts) {
var splatPrototypes : SplatPrototype[] = new SplatPrototype[tTxts.length];
for(i=0; i < tTxts.length; i++) {
var splatTxt : String[] = tTxts[i].Split("="[0]);
var splatTxtSize : String[] = splatTxt[1].Split("x"[0]);
whirld.statusTxt = "Downloading Terrain Texture " + (i + 1) + " of " + tTxts.length + " (" + splatTxt[0] + ")";
www = new WWW(whirld.getURL(splatTxt[0]));
while(!www.isDone) {
whirld.progress = www.progress;
yield new WaitForSeconds(.1);
}
if (www.error != null) whirld.info += "\nTerrain Texture Undownloadable: #" + (i + 1) + " (" + splatTxt[0] + ")";
else {
whirld.statusTxt = "Initializing Terrain Texture " + (i + 1) + " of " + tTxts.length + "...";
yield;
splatPrototypes[i] = new SplatPrototype();
splatPrototypes[i].texture = new Texture2D(4, 4, TextureFormat.DXT1, true);
www.LoadImageIntoTexture(splatPrototypes[i].texture);
splatPrototypes[i].texture.Apply(true);
splatPrototypes[i].texture.Compress(true);
splatPrototypes[i].tileSize = Vector2(parseInt(splatTxtSize[0]), parseInt(splatTxtSize[1]));
}
}
}
else {
splatPrototypes = new SplatPrototype[whirld.worldTerrainTextures.length];
for(i=0; i < whirld.worldTerrainTextures.length; i++) {
splatPrototypes[i] = new SplatPrototype();
splatPrototypes[i].texture = whirld.worldTerrainTextures[i];
splatPrototypes[i].tileSize = Vector2(15, 15);
}
}
trnDat.splatPrototypes = splatPrototypes;
//Lightmap
if(tLtmp) {
whirld.statusTxt = "Downloading Terrain Lightmap (" + tName + ")";
www = new WWW(tLtmp);
while(!www.isDone) {
whirld.progress = www.progress;
yield new WaitForSeconds(.1);
}
if (www.error != null) whirld.info += "\nTerrain Lightmap Undownloadable: " + tName + " " + tLtmp + " (" + www.error + ")";
else {
trnDat.lightmap = www.texture;
}
}
//Splatmap
if(tSpmp) {
if(tSpmp2) {
whirld.statusTxt = "Downloading Augmentative Terrain Texturemap (" + tName + ")";
www = new WWW(tSpmp2);
while(!www.isDone) {
whirld.progress = www.progress;
yield new WaitForSeconds(.1);
}
var mapColors2 = www.texture.GetPixels();
}
whirld.statusTxt = "Downloading Terrain Texturemap (" + tName + ")";
www = new WWW(tSpmp);
while(!www.isDone) {
whirld.progress = www.progress;
yield new WaitForSeconds(.1);
}
whirld.progress = 0;
whirld.statusTxt = "Mapping Terrain Textures...";
yield;
if (www.error != null) whirld.info += "\nTerrain Texturemap Undownloadable: " + tName + " " + tLtmp + " (" + www.error + ")";
else {
if (www.texture.format != TextureFormat.ARGB32 || www.texture.width != www.texture.height || Mathf.ClosestPowerOfTwo(www.texture.width) != www.texture.width) {
whirld.info += "\nTerrain Splatmap Unusable: Splatmap must be in RGBA 32 bit format, square, and it's size a power of 2";
}
else {
trnDat.alphamapResolution = www.texture.width;
var splatmapData = trnDat.GetAlphamaps(0, 0, www.texture.width, www.texture.width);
var mapColors = www.texture.GetPixels();
var ht : int = www.texture.height;
var wd : int = www.texture.width;
for (y = 0; y < ht; y++) for (x = 0; x < wd; x++) for (z = 0; z < trnDat.alphamapLayers; z++) {
if(z < 4) splatmapData[x,y,z] = mapColors[x * wd + y][z];
else splatmapData[x,y,z] = mapColors2[x * wd + y][z-4];
}
trnDat.SetAlphamaps(0, 0, splatmapData);
}
}
}
//Details (rocks, trees, grass, etc)
if(tDtmp) {
whirld.statusTxt = "Downloading Terrain Details (" + tDtmp + ")";
www = new WWW(tDtmp);
while(!www.isDone) {
whirld.progress = www.progress;
yield new WaitForSeconds(.1);
}
if (www.error != null) whirld.info += "\nTerrain Details Undownloadable: " + tName + " " + tDtmp + " (" + www.error + ")";
else {
var treePrototypes : Array = new Array();
var treeInstances : Array = new Array();
var treeProto : TreePrototype;
var detailProto : DetailPrototype;
var detailPrototypes : Array = new Array();
file = new Ionic.Zlib.GZipStream(new MemoryStream(), Ionic.Zlib.CompressionMode.Decompress).UncompressString(www.bytes).Split("\n"[0]);
for (i=0; i < file.length; i++) {
if(file[i] == "" || i == file.length - 1) { //Apply Existing Trees
if(treeProto) {
treePrototypes.Add(treeProto);
treeProto = null;
//continue;
}
if(detailProto) {
detailPrototypes.Add(detailProto);
detailProto = null;
//continue;
}
}
if(file[i].length > 10 && file[i].Substring(0, 10) == "detailmap2") {
whirld.statusTxt = "Downloading Augmentative Terrain Detail Map (" + whirld.getURL(file[i].Substring(11)) + ")";
www = new WWW(whirld.getURL(file[i].Substring(11)));
while(!www.isDone) {
whirld.progress = www.progress;
yield new WaitForSeconds(.1);
}
if (www.error != null) whirld.info += "\nAugmentative Terrain Detail Map Undownloadable: " + tName + " " + file[i].Substring(11) + " (" + www.error + ")";
else {
tex = www.texture;
pixels = www.texture.GetPixels();
if(detailPrototypes.length > 4) var detLayer4 = trnDat.GetDetailLayer(0, 0, trnDat.detailResolution, trnDat.detailResolution, 4);
if(detailPrototypes.length > 5) var detLayer5 = trnDat.GetDetailLayer(0, 0, trnDat.detailResolution, trnDat.detailResolution, 5);
if(detailPrototypes.length > 6) var detLayer6 = trnDat.GetDetailLayer(0, 0, trnDat.detailResolution, trnDat.detailResolution, 6);
if(detailPrototypes.length > 7) var detLayer7 = trnDat.GetDetailLayer(0, 0, trnDat.detailResolution, trnDat.detailResolution, 7);
i2 = 0;
for(iY = 0; iY < trnDat.detailResolution; iY++) {
for(iX = 0; iX < trnDat.detailResolution; iX++) {
if(detailPrototypes.length > 4) detLayer4[iX, iY] = Mathf.RoundToInt(pixels[i2].r * 16);
if(detailPrototypes.length > 5) detLayer5[iX, iY] = Mathf.RoundToInt(pixels[i2].g * 16);
if(detailPrototypes.length > 6) detLayer6[iX, iY] = Mathf.RoundToInt(pixels[i2].b * 16);
if(detailPrototypes.length > 7) detLayer7[iX, iY] = Mathf.RoundToInt(pixels[i2].a * 16);
i2 += 1;
}
}
}
}
else if(file[i].length > 10 && file[i].Substring(0, 9) == "detailmap") {
whirld.statusTxt = "Downloading Terrain Detail Map (" + whirld.getURL(file[i].Substring(10)) + ")";
www = new WWW(whirld.getURL(file[i].Substring(10)));
while(!www.isDone) {
whirld.progress = www.progress;
yield new WaitForSeconds(.1);
}
if (www.error != null) whirld.info += "\nTerrain Detail Map Undownloadable: " + tName + " " + file[i].Substring(10) + " (" + www.error + ")";
else {
tex = www.texture;
pixels = www.texture.GetPixels();
trnDat.detailResolution = www.texture.width;
if(detailPrototypes.length > 0) var detLayer0 = trnDat.GetDetailLayer(0, 0, trnDat.detailResolution, trnDat.detailResolution, 0);
if(detailPrototypes.length > 1) var detLayer1 = trnDat.GetDetailLayer(0, 0, trnDat.detailResolution, trnDat.detailResolution, 1);
if(detailPrototypes.length > 2) var detLayer2 = trnDat.GetDetailLayer(0, 0, trnDat.detailResolution, trnDat.detailResolution, 2);
if(detailPrototypes.length > 3) var detLayer3 = trnDat.GetDetailLayer(0, 0, trnDat.detailResolution, trnDat.detailResolution, 3);
i2 = 0;
for(iY = 0; iY < trnDat.detailResolution; iY++) {
for(iX = 0; iX < trnDat.detailResolution; iX++) {
if(detailPrototypes.length > 0) detLayer0[iX, iY] = Mathf.RoundToInt(pixels[i2].r * 16);
if(detailPrototypes.length > 1) detLayer1[iX, iY] = Mathf.RoundToInt(pixels[i2].g * 16);
if(detailPrototypes.length > 2) detLayer2[iX, iY] = Mathf.RoundToInt(pixels[i2].b * 16);
if(detailPrototypes.length > 3) detLayer3[iX, iY] = Mathf.RoundToInt(pixels[i2].a * 16);
i2 += 1;
}
}
}
}
else if(file[i] == "tree") { //Create new tree prototype
treeProto = new TreePrototype();
}
else if(file[i] == "detail") {
detailProto = new DetailPrototype();
}
else if(treeProto) {
if(file[i].Substring(0, 1) == "m") {
//treeProto.prefab = Resources.Load(file[i].Substring(2));
//treeProto.prefab = Game.Controller.prefab;
//prefab.AddComponent(MeshFilter);
//prefab.AddComponent(MeshRenderer);
if(objects.ContainsKey(file[i].Substring(2))) {
treeProto.prefab = prefabs.Pop();
treeProto.prefab.name = file[i].Substring(2);
if(!treeProto.prefab.GetComponent(MeshFilter)) treeProto.prefab.AddComponent(MeshFilter);
treeProto.prefab.GetComponent(MeshFilter).mesh = objects[file[i].Substring(2)].GetComponent(MeshFilter).mesh;
if(!treeProto.prefab.GetComponent(MeshRenderer)) treeProto.prefab.AddComponent(MeshRenderer);
treeProto.prefab.GetComponent(MeshRenderer).materials = objects[file[i].Substring(2)].GetComponent(MeshRenderer).materials;
//treeProto.prefab = objects[file[i].Substring(2)];
}
else whirld.info += "\nTerrain Detail Mesh not found: " + file[i].Substring(2);
}
else if(file[i].Substring(0, 1) == "b") treeProto.bendFactor = parseFloat(file[i].Substring(2));
else if(file[i+1] == "") { //Read detail objects
var treedat : String[] = file[i].Split(";"[0]);
for(var tr : String in treedat) {
if(!tr) continue;
var tree = tr.Split(","[0]);
if(!tree[0] || tree[0] == "") continue;
var treeInstance = new TreeInstance();
treeInstance.prototypeIndex = treePrototypes.length;
treeInstance.position = Vector3(parseFloat(tree[0]), parseFloat(tree[1]), parseFloat(tree[2]));
treeInstance.widthScale = parseFloat(tree[3]);
treeInstance.heightScale = parseFloat(tree[4]);
var c : float = Random.Range(.6, .7);
treeInstance.color = Color(c - .15, c - .15, c - .15, 1);
treeInstance.lightmapColor = Color(c + .15, c + .15, c + .15, 0);
treeInstances.Add(treeInstance);
}
}
}
else if(detailProto) {
l = file[i].Split(" "[0]);
if(l[0] == "pO") {
if(objects.ContainsKey(l[1])) {
//detailProto.prototype = objects[l[1]];
detailProto.prototype = prefabs.Pop();
detailProto.prototype.name = l[1];
if(!detailProto.prototype.GetComponent(MeshFilter)) detailProto.prototype.AddComponent(MeshFilter);
detailProto.prototype.GetComponent(MeshFilter).mesh = objects[l[1]].GetComponent(MeshFilter).mesh;
if(!detailProto.prototype.GetComponent(MeshRenderer)) detailProto.prototype.AddComponent(MeshRenderer);
detailProto.prototype.GetComponent(MeshRenderer).material = objects[l[1]].GetComponent(MeshRenderer).material;
}
else whirld.info += "\nTerrain Detail Mesh not found: " + l[1];
}
else if(l[0] == "pT") {
l[1] = whirld.getURL(l[1]);
whirld.statusTxt = "Downloading Terrain Detail Texture (" + l[1] + ")";
www = new WWW(l[1]);
while(!www.isDone) {
whirld.progress = www.progress;
yield new WaitForSeconds(.1);
}
if (www.error != null) whirld.info += "\nTerrain Detail Texture Undownloadable: " + l[1];
else {
whirld.statusTxt = "Initializing " + vS[0] + "...";
whirld.progress = 0;
yield;
mshTxt = new Texture2D(4, 4, TextureFormat.DXT5, true);
www.LoadImageIntoTexture(mshTxt);
mshTxt.Apply(true);
mshTxt.Compress(true);
mshTxt.wrapMode = TextureWrapMode.Clamp;
detailProto.prototypeTexture = mshTxt;
}
}
else if(l[0] == "minW") detailProto.minWidth = parseFloat(l[1]);
else if(l[0] == "maxW") detailProto.maxWidth = parseFloat(l[1]);
else if(l[0] == "minH") detailProto.minHeight = parseFloat(l[1]);
else if(l[0] == "maxH") detailProto.maxHeight = parseFloat(l[1]);
else if(l[0] == "nS") detailProto.noiseSpread = parseFloat(l[1]);
else if(l[0] == "bF") detailProto.bendFactor = parseFloat(l[1]);
else if(l[0] == "hC") detailProto.healthyColor = Color(parseFloat(l[1]), parseFloat(l[2]), parseFloat(l[3]));
else if(l[0] == "dC") detailProto.dryColor = Color(parseFloat(l[1]), parseFloat(l[2]), parseFloat(l[3]));
else if(l[0] == "lF") detailProto.lightmapFactor = parseFloat(l[1]);
else if(l[0] == "gL") detailProto.grayscaleLighting = (l[1] == "1" ? true : false);
else if(l[0] == "rM") {
if(l[1] == "GrassBillboard") detailProto.renderMode = DetailRenderMode.GrassBillboard;
else if(l[1] == "VertexLit") detailProto.renderMode = DetailRenderMode.VertexLit;
else detailProto.renderMode = DetailRenderMode.Grass;
}
else if(l[0] == "uM") detailProto.usePrototypeMesh = (l[1] == "1" ? true : false);
}
}
trnDat.treePrototypes = treePrototypes.ToBuiltin(TreePrototype);
trnDat.treeInstances = treeInstances.ToBuiltin(TreeInstance);
trnDat.detailPrototypes = detailPrototypes.ToBuiltin(DetailPrototype);
if(detailPrototypes.length > 0) trnDat.SetDetailLayer(0, 0, 0, detLayer0);
if(detailPrototypes.length > 1) trnDat.SetDetailLayer(0, 0, 1, detLayer1);
if(detailPrototypes.length > 2) trnDat.SetDetailLayer(0, 0, 2, detLayer2);
if(detailPrototypes.length > 3) trnDat.SetDetailLayer(0, 0, 3, detLayer3);
if(detailPrototypes.length > 4) trnDat.SetDetailLayer(0, 0, 4, detLayer4);
if(detailPrototypes.length > 5) trnDat.SetDetailLayer(0, 0, 5, detLayer5);
if(detailPrototypes.length > 6) trnDat.SetDetailLayer(0, 0, 6, detLayer6);
if(detailPrototypes.length > 7) trnDat.SetDetailLayer(0, 0, 7, detLayer7);
//trnDat.RefreshPrototypes();
//trnDat.RecalculateTreePositions();
}
/*for(i=0; i<trnDat.treePrototypes.length; i++) {
trnDat.treePrototypes[i].prefab.name = trnDat.treePrototypes[i].prefab.name.Replace(" ", "_");
MeshWriteObj(trnDat.treePrototypes[i].prefab.GetComponent(MeshFilter), trnDat.treePrototypes[i].prefab.name);
data = "[msh:" + trnDat.treePrototypes[i].prefab.name + "," + trnDat.treePrototypes[i].prefab.name + ".obj.gz]" + data;
trnV += (trnV != "" ? "\n\n" : "") + "tree\nm:" + trnDat.treePrototypes[i].prefab.name + "\nb:" + trnDat.treePrototypes[i].bendFactor + "\n";
for(var tree : TreeInstance in trnDat.treeInstances) {
if(tree.prototypeIndex != i) continue;
trnV += tree.position.x.ToString("F1", System.Globalization.CultureInfo.InvariantCulture) + "," + tree.position.y.ToString("F1", System.Globalization.CultureInfo.InvariantCulture) + "," + tree.position.z.ToString("F1", System.Globalization.CultureInfo.InvariantCulture) + "," + tree.widthScale.ToString("F1", System.Globalization.CultureInfo.InvariantCulture) + "," + tree.heightScale.ToString("F1", System.Globalization.CultureInfo.InvariantCulture) + ";";
}
}*/
}
//Go!
var trnObj : GameObject = new GameObject(tName);
trnObj.AddComponent(Terrain);
trnObj.GetComponent(Terrain).terrainData = trnDat;
trnObj.AddComponent(TerrainCollider);
trnObj.GetComponent(TerrainCollider).terrainData = trnDat;
objects.Add(tName, trnObj);
GameObject.Destroy(trnObj);
whirld.statusTxt = "";
}
i++;
}
Thanks - the code looks pretty straightforward. I'll have to play with code that modifies the Terrain object's hidden properties at runtime to see whether dynamic changes (e.g. chop down a tree) are reflected immediately, or whether it's going to involve rebuilding and reloading the entire terrain object. The latter is, obviously, not very practical, even if I make the assumption that I can modify my code easily enough to keep up with changes in the implementation.
Answer by Tom 1 · Jan 25, 2010 at 08:16 PM
You can modify almost anything about terrain at runtime. However, as Jaap already outlined, the API is largely undocumented. But textures (splatmaps), trees, grass and detail meshes are all scriptable.
Yes, chopping down a tree is instant. If you search the forum or look at my website, I created a script to destroy trees at runtime.
Answer by Sean Baggaley · Jan 27, 2010 at 10:01 AM
Just a heads-up: The script Jaap Kreijkamp found appears to be from the "Whirld" project on the Unify Community wiki.
And there is also Terrain Destruction which does some simple terrain digging.