Skip to content

Instantly share code, notes, and snippets.

@druggedhippo
Last active April 15, 2024 03:15
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save druggedhippo/76febb3e1a7375e49ff0180cebcbc0eb to your computer and use it in GitHub Desktop.
Save druggedhippo/76febb3e1a7375e49ff0180cebcbc0eb to your computer and use it in GitHub Desktop.
Unity Navigation mesh by terrain texture
/****
NOTE NOTE NOTE
1. Add this component to a terrain
2. Set your area ID's to be what you want the agent to think each surface in your terrain is (eg, Grass, Path), in the same order as the textures assigned in the terrain textures
3. Set "Defaultarea" to be whatever texture you expect to be the "most" common terrain texture. Generall this will be the first texture you assigned in step 1, eg "Grass"
IF THIS IS STILL TO SLOW:
- Adjust step to 2, 5, 10 or something higher
*/
using Sirenix.OdinInspector;
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.AI.Navigation;
using UnityEngine;
using UnityEngine.AI;
/** If this script seems to take forever, CHECK THE LAYERS YOUR NAV AGENT IS USING and filter out all the crap you don't need */
[RequireComponent(typeof(NavMeshSurface))]
public class NavMeshGenA : MonoBehaviour
{
Terrain terrain;
TerrainData terrainData;
Vector3 terrainPos;
/** This NEEDS to be a layer used by your Navigation agent */
public String NavAgentLayer = "Default";
public String defaultarea = "Walkable";
/** Include trees collision in navigation mesh */
public bool includeTrees;
public float timeLimitInSecs = 20;
/** How detailed will the edges of the terrain texture navmesh be? Smaller values mean larger generation times */
public int step = 1;
/** This should match the order of the terrain textures in use */
public List<string> areaID;
// Start is called before the first frame update
void Start()
{
}
[Button("Generate NavAreas")]
void GenMeshes()
{
terrain = GetComponent<Terrain>();
terrainData = terrain.terrainData;
terrainPos = terrain.transform.position;
Vector3 size = terrain.terrainData.size;
Vector3 tpos = terrain.GetPosition();
float minX = tpos.x;
//tc.bounds.min.x;
float maxX = minX + size.x;
float minZ = tpos.z;
float maxZ = minZ + size.z;
GameObject attachParent;
Transform childA = terrain.transform.Find("Delete me");
if (childA != null)
{
attachParent = childA.gameObject;
}
else
{
attachParent = new GameObject();
attachParent.name = "Delete me";
attachParent.transform.SetParent(terrain.transform);
attachParent.transform.localPosition = Vector3.zero;
}
int terrainLayer = LayerMask.NameToLayer(NavAgentLayer);
int defaultWalkableArea = NavMesh.GetAreaFromName(defaultarea);
//avMesh.GetAreaFromName("Walkable");
Debug.Log("terrain pos:" + tpos);
Debug.Log("terrain size:" + size);
Debug.Log("minX:" + minX + ", maxX:" + maxX + ", minZ:" + minZ + ", maxZ:" + maxZ);
// get the splat data for this cell as a 1x1xN 3d array (where N = number of textures)
float[,,] splatmapData = terrainData.GetAlphamaps(0,0, terrainData.alphamapWidth, terrainData.alphamapHeight);
Debug.Log("alpha h width:" + terrainData.alphamapWidth + ", height:" + terrainData.alphamapHeight + ", resolution:" + terrainData.alphamapResolution);
float alphaWidth = terrainData.alphamapWidth;
float alphaHeight = terrainData.alphamapHeight;
float tWidth = terrainData.size.x;
float tHeight = terrainData.size.z;
float startTime = Time.realtimeSinceStartup;
float xStepsize = tWidth * ((float)1 / (float)alphaWidth);
float zStepsize = tHeight * ((float)1 / (float)alphaHeight);
Debug.Log("xStepSize:" + xStepsize);
for (int dx = 0; dx <= alphaWidth; dx += step)
{
float xOff = tWidth * ((float)dx / (float)alphaWidth);
for (int dz = 0; dz <= alphaHeight; dz += step)
{
float zOff = tHeight * ((float)dz / (float)alphaHeight);
try
{
int surface = GetMainTextureA(dz, dx, ref splatmapData);
int navArea = defaultWalkableArea;
if (areaID.Count > surface)
navArea = NavMesh.GetAreaFromName(areaID[surface]);
if (navArea == defaultWalkableArea)
continue;
if (Time.realtimeSinceStartup > startTime + timeLimitInSecs)
{
Debug.Log("Time limit exceeded");
goto escape;
}
//if (true)
// continue;
Vector3 pos = new Vector3(minX + xOff, 0, minZ + zOff);
// Create a cube "step size" that is "slightly" above the height and mark it
GameObject obj = new GameObject();
//GameObject.CreatePrimitive(PrimitiveType.Cube);
Transform objT = obj.transform;
objT.SetParent(attachParent.transform);
objT.localScale = Vector3.one;
objT.gameObject.layer = terrainLayer;
float height = terrain.SampleHeight(pos);
objT.position = new Vector3(pos.x, height, pos.z);
obj.isStatic = true;
NavMeshModifierVolume nmmv = obj.AddComponent<NavMeshModifierVolume>();
nmmv.size = new Vector3(xStepsize*step, 1, zStepsize * step);
nmmv.center = Vector3.zero;
nmmv.area = navArea;
}
catch (Exception e)
{
}
}
}
escape:
//
if (includeTrees)
{
Debug.Log("Now doing trees");
TreeInstance[] instances = terrainData.treeInstances;
TreePrototype[] prototypes = terrainData.treePrototypes;
Vector3 tsize = terrainData.size;
foreach (TreeInstance inst in instances)
{
TreePrototype prototype = prototypes[inst.prototypeIndex];
Vector3 pos = (Vector3.Scale(inst.position, tsize));
float rotY = inst.rotation;
float hscale = inst.heightScale;
float wscale = inst.widthScale;
//Debug.Log("tree: " + (Vector3.Scale(pos, tsize)) + ", rot:" + rotY + ", hscale:" + hscale + ", wscale:" + wscale);
GameObject tree = GameObject.Instantiate(prototype.prefab);
Transform objT = tree.transform;
objT.SetParent(attachParent.transform);
objT.position = new Vector3(pos.x, pos.y, pos.z);
objT.localRotation = Quaternion.Euler(0, Mathf.Deg2Rad * rotY, 0);
objT.localScale = new Vector3(wscale, hscale, wscale);
objT.gameObject.layer = terrainLayer;
tree.isStatic = true;
}
}
Debug.Log("Done prep, build nav mesh");
foreach (NavMeshSurface nsurface in GetComponents<NavMeshSurface>())
{
//NavMeshSurface nsurface = GetComponent<NavMeshSurface>();
nsurface.BuildNavMesh();
}
Debug.Log("Finished, destroy our temp objects");
GameObject.DestroyImmediate(attachParent.gameObject);
}
void destroyChildren(Transform attachParent)
{
// Delete children from nav areas
while (attachParent.childCount > 0)
GameObject.DestroyImmediate(attachParent.GetChild(0).gameObject);
}
// Update is called once per frame
void Update()
{
}
/** https://answers.unity.com/questions/456973/getting-the-texture-of-a-certain-point-on-terrain.html */
private float[] GetTextureMixA(int x, int z, ref float[,,] splatmapData)
{
// extract the 3D array data to a 1D array:
float[] cellMix = new float[splatmapData.GetUpperBound(2) + 1];
for (int n = 0; n < cellMix.Length; n++)
{
cellMix[n] = splatmapData[x, z, n];
}
return cellMix;
}
private int GetMainTextureA(int x, int z, ref float[,,] splatmapData)
{
// returns the zero-based index of the most dominant texture
// on the main terrain at this world position.
float[] mix = GetTextureMixA(x, z, ref splatmapData);
float maxMix = 0;
int maxIndex = 0;
// loop through each mix value and find the maximum
for (int n = 0; n < mix.Length; n++)
{
if (mix[n] > maxMix)
{
maxIndex = n;
maxMix = mix[n];
}
}
return maxIndex;
}
}
@shasaur
Copy link

shasaur commented Jan 20, 2022

Hey, I found this from this forum thread. I tried using it with my terrain but it took 20+ minutes to generate so I just interrupted it by closing Unity.

Do you think that's expected with my terrain size?

  • Terrain Width: 1000
  • Terrain Length: 1000
  • Terrain Height: 600
  • Detail Resolution Per Patch: 32
  • Detail Resolution: 2048

@druggedhippo
Copy link
Author

druggedhippo commented Jan 20, 2022

@shasaur Hi, this works by creating boxes every X units across the terrain.

If you used the default step size of "1", then Unity is trying to create 1million (1000x1000) boxes and doing raytraces on each, so Unity isn't very happy with you.

If you increase the step size to 100 then it'll finish in a minute or so, since then it's only making 1000 boxes, but then your nav resolution will be rather larger and not as fine as you may want.. You can also try step size of 10

There are clearly avenues for optimization here, grouping same texture areas together into a single larger volume to reduce the number of objects for example, but at the time this GIST served my purpose.

--

In the immediate now though, if you set step to 10 and comment out line 139 it won't actually DELETE the navigation volumnes it's using to create the nav hints, so you can then customize them, resize them, add your own to fix up those bits around your areas. Then you can use the "Bake" button on the "NaveMeshSurface" component and it'll use the boxes that are in place as it's navigation hints.

--

I also see I made a mistake at line 89, it should be:

objT.localScale = Vector3.one;

@shasaur
Copy link

shasaur commented Jan 20, 2022

Wow that actually kinda worked, well-written!

Here's with a step size of 10:
image

And a step size of 5 (though takes significantly longer):
image

Even if they don't fully connect, I imagine I could use the step size 5 one because the agent will essentially interpolate the path between the squares which should look accurate enough.

@druggedhippo
Copy link
Author

druggedhippo commented Jan 21, 2022

@shasaur Hi, I've updated this gist with a newer version that is signifinicantly quicker than the previous one. A 1000x1000 grid is now generating under 10 seconds for me even at step 1. (I also added a timeout check so it should exit early instead of locking up unity)

Set your areas like before, but now also set the "DefaultArea" which is what the predominate Navigation Area will be, which will probably be the same as the firste area ID.

image

@shasaur
Copy link

shasaur commented Jan 22, 2022

Wow thanks for the updates; it's awesome to see you iterate with such great changes so quickly!

I tried re-running it with the same settings but it's no longer picking up my dirt path, maybe I've misconfigured my layers or area IDs?

image

@druggedhippo
Copy link
Author

druggedhippo commented Jan 22, 2022

@shasaur

Under Advanced, I'm going to guess your NavMeshSurface agent setting "Default Area" is set to "Walkable" .

And, your first terrain area id is set to "Grassy", and you have "Grassy" in "Defaultarea". What this means in effect is that the generation code ignores it because it thinks it's the same as the NavMeshSurface agent, which means no modifier volume is created and Unity will automatically assign that location to "Walkable" when it builds the navmesh. This is at line 114, if the detected texture (Grassy) matches the specified "Defaultarea" value (in your case Grassy), it simply won't put a volume modifier, so it becomes Walkable by default.

Your Dirt_B is not being ignored, but it's being set to "Walkable", which the same as default NavMeshSurface agent setting, which is why your Grassy texture and Dirt_B textures are both having Walkable (blue) assigned to them.

The best way of fixing is is not to use Walkable in any of the other areas, use a different agent layer like "Path".

--

If you change the "Defaultarea" setting to Walkable, but then the script will try to generate a million checks again because the default texture isn't Walkable, it's Grass.

--

Alternately you can change the first Area ID to "Walkable", the Default area to "Walkable" and leave "Walkable" as the "Advanced -> Default Area" setting.

@shasaur
Copy link

shasaur commented Jan 22, 2022

Yeah you're right, I think for some reason I thought I couldn't change the default area cost so I decided to have this work around. But I've changed to have the 'Walkable' area be the default as you suggested and adjusted the area costs and now it works perfectly!

image

Thanks for this!

I still need to mess around with the path thickness and probably use a different texture for grass detail shading (so I don't get the random squares), but this works very well! Excited to try this out with the NavMesh agents.

@samolulu
Copy link

samolulu commented Jul 8, 2022

hi。i got a crash when generate navareas on Unity 2021.2.19f1. i think it happen because my terrain has no texture layer.

@fabgames5
Copy link

fabgames5 commented Jan 19, 2023

Worked great Thank You..
to those crashing Unity.. it happened to me a few times before I got the settings correct.
Area ID needs to be your Navmesh Layers (Names) corresponding to how they match to your Terrain layers. once I looked at shasaur image I realized my mistake and had it working. I'm trying to do this during runtime as I update connecting paths so I'm going try and spread the load out over a few seconds , as I don't need the navmesh update right away (as that wont be noticeable to players during the game) but don't want the 1-3 sec lag (Frozen screen) it takes to update. if I see a good improvement ill add it here..

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment