Procedural Landscape or how to Shatter a Planet! (with actual code examples)

posted in Septopus for project Unsettled World
Published October 07, 2018
Advertisement

I've spent the last few days coding in my Economy Server, I've almost completed the functionality of Banking! 

So, it's truly time for a rewind about something entirely different to clear my head. ;)

uwBeachyED01.thumb.png.9913daad10dc65812ae6ed04bb217943.png

Here's an editor view from a little while back, before I was loading a "visible planet" object, the procedurally loaded landscape was the only visible landscape, the rest of the planet, if you could see it before the landscape loaded, just looked like water.

So, let me start off by clarifying, the landscape in my game is generated prodedurally, however it is not "generated" during game play.

I have a separate process that I can run on any sculptured sphere of my choosing that will generate the individual triangular mesh objects and then save them into "tile" files.  These tile files also contain place holders for items such as Arrays of Trees, Rocks, and Plants.

Let's start at the Crawler!  The following script is the code that crawls from triangle to triangle along the planet mesh and kicks off the generation of the tiles.


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MeshCrawler : MonoBehaviour {

    public Vector3[] verts;
    public int[] tris;
    public Vector3[] norms;
    public int curTri = 0;
    public int startTri = -1;
    public int endTri;
    public string meshName;
    public List<int> triBuffer = new List<int>();
    bool crawling = false;

	// Use this for initialization
	void Start () {
        verts = GetComponent<MeshFilter>().mesh.vertices;
        norms = GetComponent<MeshFilter>().mesh.normals;
        tris = GetComponent<MeshFilter>().mesh.triangles;
        meshName = GetComponent<MeshFilter>().mesh.name;
    }

    IEnumerator CrawlIT()
    {
        crawling = true;
        if (endTri > (tris.Length / 3)) endTri = tris.Length / 3;
        if (startTri > -1 && endTri > 0)
        {
            Debug.Log("Crawling " + startTri.ToString() + " to " + endTri.ToString());
            for (int i = startTri; i < endTri; i++)
            {
                curTri = i;
                Vector3 p0 = verts[tris[i * 3 + 0]];
                Vector3 p1 = verts[tris[i * 3 + 1]];
                Vector3 p2 = verts[tris[i * 3 + 2]];
                Vector3 n0 = norms[tris[i * 3 + 0]];
                Vector3 n1 = norms[tris[i * 3 + 1]];
                Vector3 n2 = norms[tris[i * 3 + 2]];

                UWTerrainTri triCandidate = new UWTerrainTri();
                triCandidate.triIndex = i;
                triCandidate.meshName = "PlanetZed";
                triCandidate.points = new Vector3[3] { p0, p1, p2 };
                triCandidate.normals = new Vector3[3] { n0, n1, n2 };
                TerrainController.TriHit(triCandidate);
                //Debug.Log("Tri: " + i.ToString());
                yield return null;
            }
        }
        Debug.Log("Crawler finished.");
        crawling = false;
    }
	
	// Update is called once per frame
    void Update () {
        if (triBuffer.Count > 0)
        {
            if (!crawling)
            {
                startTri = triBuffer[0] - 5;
                endTri = triBuffer[0] + 5;
                run = true;
                triBuffer.RemoveAt(0);
            }
        }
        if (!crawling)
        {
            StartCoroutine(CrawlIT());
        }
    }
}

Okay, so what's it doing?

This script is attached to the planet mesh object and it starts a Coroutine(Unity safe psudo-thread a "function that yields") that handles all the data gathering steps.

This routine grabs the triangles array from the MeshFilter and works its way from 0 to the end, pulling the vertices out of the MeshFilters verts array, stuffing those into a class that is passed to another bit of code. 

The following code decides how to handle the UWTerrainTri class objects that are passed from the Crawler:


//The above code sends it's result to a "TriHit()" routine.
//that routine has terrain Loading functionality in it, in this mode mode
//it just passes the UWTerrainTri object to GenTriLand():

public List<Vector3> vertsWTrees = new List<Vector3>();

public void GenTriLand(UWTerrainTri uWTerrainTri)
    {
  		//Create a new mesh object.
        Mesh mesh = new Mesh();
  		//This is the class that is saved as the "tile" object.
        TerrainSegment ts = new TerrainSegment();
  		
  		//Generate a good file name.
        string tpName = uWTerrainTri.meshName + uWTerrainTri.triIndex.ToString("0000000");
        //Grab the vertices.
  		ts.vertices = uWTerrainTri.points;
		
  		//500,000 files in the same directory is a BAD idea. ;)
        string dir1 = tpName.Substring(9, 2);
        string dir2 = tpName.Substring(11, 1);
        dir2 = "0" + dir2;
        
  		//Generate a full file name.
  	    string fullFilePath = Application.persistentDataPath + "/tdat/" + dir1 + "/" + dir2 + "/" + tpName + ".dat";
        
        mesh.vertices = ts.vertices;
        mesh.uv = new Vector2[3] { new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 1) };
        mesh.triangles = new int[] { 0, 1, 2 };
        mesh.RecalculateBounds();
        mesh.RecalculateNormals();
		
  		ts.vertices = mesh.vertices;
        ts.normals = mesh.normals;
        ts.uvs = mesh.uv;
        ts.Tris = mesh.triangles;
       
  		//Data Objects for Rocks, Grass, Trees
 		List<EnvironmentObject> rl = new List<EnvironmentObject>();
      	List<EnvironmentObject> gl = new List<EnvironmentObject>();
      	List<EnvironmentObject> tl = new List<EnvironmentObject>();
      	List<Vector3> usedVertices = new List<Vector3>();
  		//Rocks
  		int r = UnityEngine.Random.Range(3, 12);
  		for (int i = 0; i < r; i++)
     	{
    	   Vector3 point = GetRandomPointOnMesh(mesh);
       
    	   int tryCount = 0;
    	   while (usedVertices.Contains(point))
    	   {
             	tryCount++;
  				point = GetRandomPointOnMesh(mesh);
             	if (tryCount > 10) goto NoRock;//Yeah goto just works sometimes, hate me if you want.
           }
           usedVertices.Add(point);
           EnvironmentObject eo = new EnvironmentObject();
           eo.prefab = "rock" + UnityEngine.Random.Range(1, 3).ToString() + "sm";
           eo.pos = point;
           eo.rot = Quaternion.identity;
           eo.scale = Vector3.one;
           rl.Add(eo);
           NoRock:;
       }
       usedVertices.Clear();
  		//Grass
       int g = UnityEngine.Random.Range(2, 8);
       for (int i = 0; i < g; i++)
       {
           Vector3 point = GetRandomPointOnMesh(mesh);
           int tryCount = 0;
           while (usedVertices.Contains(point))
           {
               tryCount++;
               point = GetRandomPointOnMesh(mesh);
               if (tryCount > 10) goto NoGrass;
           }
           usedVertices.Add(point);
           EnvironmentObject eo = new EnvironmentObject();
           eo.prefab = "grass" + UnityEngine.Random.Range(1, 4).ToString();
           eo.pos = point;  // + new Vector3(UnityEngine.Random.Range(-5.0f, 5.0f), 0, UnityEngine.Random.Range(-5.0f, 5.0f));
           eo.rot = Quaternion.identity;
           eo.scale = Vector3.one;
           gl.Add(eo);
           NoGrass:;
       }
       usedVertices.Clear();
  		//Trees
       int t = UnityEngine.Random.Range(0, 3);
       for (int i = 0; i < t; i++)
       {
           Vector3 point = mesh.vertices[UnityEngine.Random.Range(0, mesh.vertices.Length)];
           int tryCount = 0;
           while (vertsWTrees.Contains(point))
           {
               tryCount++;
               point = mesh.vertices[UnityEngine.Random.Range(0, mesh.vertices.Length)]; 
               if (tryCount > 10) goto NoTree;
           }
           vertsWTrees.Add(point);
           EnvironmentObject eo = new EnvironmentObject();
           eo.prefab = "tree" + UnityEngine.Random.Range(1, 3).ToString();
           eo.pos = point;
           GameObject tree = new GameObject();
           tree.transform.SetParent(planet.transform);
           tree.transform.localPosition = eo.pos;
           float maxs = 10;
           if (eo.prefab == "tree2")
               maxs = 15;
           FixTree(tree.transform, 5, maxs, 10);
           eo.pos = tree.transform.localPosition;
           eo.rot = tree.transform.localRotation;
           eo.scale = tree.transform.localScale;
           Destroy(tree);
           tl.Add(eo);
           NoTree:;
      }
  		//Housekeeping for performance.
      if (vertsWTrees.Count > 10000)
        {
           while (vertsWTrees.Count > 10000)
           {
               vertsWTrees.RemoveAt(0);
           }
       }
       ts.rocks = rl.ToArray();
       ts.grass = gl.ToArray();
       ts.trees = tl.ToArray();

  		//Save terrain "tile" file.
       string json = JsonUtility.ToJson(ts);
       File.WriteAllText(fullFilePath, json);
       //Debug.Log("New Terrain Generated: " + tpName);
    }

[Serializable]
public class TerrainSegment
{
    public Vector3[] vertices;
    public Vector3[] normals;
    public Vector2[] uvs;
    public int[] Tris;
    public EnvironmentObject[] rocks;
    public EnvironmentObject[] grass;
    public EnvironmentObject[] trees;
}
[Serializable]
public class EnvironmentObject
{
    public string prefab;
    public Vector3 pos;
    public Quaternion rot;
    public Vector3 scale;
}

This code produces a file for each triangle in the planet mesh.  These files are then "streamed" to the client when any of many "mesh agents"(attached to the player object) in the game does a raycast at the ground and discovers a triangle ID that has not been loaded.  The game client will then send a request to the server to download the tile number that hasn't been found in the local cache of tile files.

Which as you can see from the above image, it works pretty swimmingly.

Let me know what ya think! ;)

 

1 likes 0 comments

Comments

Awoken

Interesting.  It's neat to learn about the technique you've used, we are definitely tackling similar problems.

October 08, 2018 07:52 PM
Septopus
1 hour ago, Awoken said:

Interesting.  It's neat to learn about the technique you've used, we are definitely tackling similar problems.

For sure, I've been reading up on your project too.  It's cool to see how somebody else goes about a project of this scope/magnitude.  I like what you got so far, simulations are my all time favorite.  I've started to build them in the past but never got past moving the Sims around, and those weren't online either.  So, c'mon hurry it up, I wanna play!

October 08, 2018 09:43 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement