Find edge/surface/ledge of mesh

Hello, I’ve been messing around with my character & i need to find out a way to detect if there’s an edge/surface/ledge so the character can grab on to it.

Take the image below for example

I would like to detect the areas that are lined red & that are an edge/surface using C# so the character can grab on to it.

Any help would be appreciated, thanks!

  • Jabez

Is this for a render effect or do you need them for script logic? Is this intended for character climbing?

Seems like it may be for base building? ie need only certain objects to snap at certain locations

Thanks for the replies. Tis for character climbing, I need it to be dynamic.

Dynamic meaning that the mesh itself changes?

Dynamic meaning that it can detect the edge/surface/ledge of any mesh.

3 Answers

3

Since the mesh is going to be dynamic, I suggest detecting the edge on the fly with casts. See the following picture:

You do a sphere cast (or horizontal raycast alternatively) above the character to detect if there is no vertical surface to climb to. You do a vertical raycast above and forward to detect if it is an actual edge. I used this technique successfully for a similar problem.

This approach does assume that you are generating/updating a collider mesh that matches the rendered mesh, or a proxy for that mesh (e.g. set of box colliders). If you have control over the colliders you might also want to separate the vertical and horizontal ones and tagged them differently for more specific checks.

Thanks for this information i actually saw a video that used a similar technique, I'll try it out a bit later when i get time & let you know how it goes :D.

thumbs up for drawing!

So I have been trying to do something with this all day long. And I managed to get the quad that the player hits, get the top edge of it and check if it is climable. I think it only works with perfect quads, and its not very good at all, but it works. So its far from perfect but its a good base for you to start building on!

using UnityEngine;
using System.Collections;
//using System.Collections.Generic;
//using System.Threading;

//TODO Clean up stuff. Fix bug and tweak stuff. Optimize stuff. Animation support. Add climbing support. Make usable for non-perfect quads. Find all edges within reach instead of just the closest.
//Made in a day by Imre Angelo aka Lahzar @ the unity forums - Enjoy!
public class RunFree3o : MonoBehaviour {
		public float reach = 3;	//TODO Check from players arms, legs etc
private Vector3[] quad;
private Vector3[] top;

void Start()	{
	quad = new Vector3[4];
}

void Update()	{
	if(top[0] != new Vector3())
		Debug.DrawLine(top[0], top[1], Color.green, 1);
}

void OnControllerColliderHit(ControllerColliderHit col)	{
	RaycastHit hit;
	Debug.DrawRay(col.point, col.moveDirection, Color.blue, 1);
	if(Physics.Raycast(transform.position, col.moveDirection, out hit, 0.5f))	{
		if(hit.normal.y == transform.position.y-(GetComponent<CharacterController>().height/2))
			return;

		MeshCollider meshCollider = hit.collider as MeshCollider;
		if (meshCollider == null || meshCollider.sharedMesh == null)
			return;
		
		Mesh mesh = meshCollider.sharedMesh;
		Vector3[] vertices = mesh.vertices;
		int[] triangles = mesh.triangles;
		Transform hitTransform = hit.collider.transform;

		StartCoroutine(CoroutineThing(mesh, hitTransform, vertices, triangles, hit.triangleIndex));
	}
}

IEnumerator CoroutineThing(Mesh mesh, Transform hitTransform, Vector3[] vertices, int[] triangles, int triangleIndex)	{
	#region Get hit triangle
	Vector3 p0 = vertices[triangles[triangleIndex * 3 + 0]];
	Vector3 p1 = vertices[triangles[triangleIndex * 3 + 1]];
	Vector3 p2 = vertices[triangles[triangleIndex * 3 + 2]];
	p0 = hitTransform.TransformPoint(p0);
	p1 = hitTransform.TransformPoint(p1);
	p2 = hitTransform.TransformPoint(p2);
	#endregion

	Debug.DrawLine(p0, p1, Color.blue, 4);
	Debug.DrawLine(p1, p2, Color.blue, 4);
	Debug.DrawLine(p2, p0, Color.blue, 4);

	#region Get last triangle
	Vector3 p3 = new Vector3();
	for(int i = 10; i > -10; i--)	{
		Vector3 p4 = new Vector3();
		if(vertices[triangles[triangleIndex * 3 + i]] == null)
			continue;

		try {
			p4 = hitTransform.TransformPoint(vertices[triangles[triangleIndex * 3 + i]]);
		} catch (System.Exception ex)	{
			ex.Source = "ignore";
			continue;
		}

		if(p4 != p0 && p4 != p1 && p4 != p2)	{

			int ex = 0, wai = 0, zed = 0;
			if(Mathf.FloorToInt(p4.x) == Mathf.FloorToInt(p0.x))	{	ex++;	}
			if(Mathf.FloorToInt(p4.y) == Mathf.FloorToInt(p0.y))	{	wai++;	}
			if(Mathf.FloorToInt(p4.z) == Mathf.FloorToInt(p0.z))	{	zed++;	}
			if(Mathf.FloorToInt(p4.x) == Mathf.FloorToInt(p1.x))	{	ex++;	}
			if(Mathf.FloorToInt(p4.y) == Mathf.FloorToInt(p1.y))	{	wai++;	}
			if(Mathf.FloorToInt(p4.z) == Mathf.FloorToInt(p1.z))	{	zed++;	}
			if(Mathf.FloorToInt(p4.x) == Mathf.FloorToInt(p2.x))	{	ex++;	}
			if(Mathf.FloorToInt(p4.y) == Mathf.FloorToInt(p2.y))	{	wai++;	}
			if(Mathf.FloorToInt(p4.z) == Mathf.FloorToInt(p2.z))	{	zed++;	}
			
			if(ex+wai+zed == 5)	{
				if(wai == 1)	{
					p3 = p4;
					break;
				}
			}
		}
	}
	#endregion
	
	Debug.DrawLine(p0, p3, Color.red, 4);
	Debug.DrawLine(p1, p3, Color.red, 4);
	Debug.DrawLine(p2, p3, Color.red, 4);

	if(p3 != new Vector3())	{
		quad = new Vector3[4];
		quad[3] = p3;
	}

	quad[0] = p0;
	quad[1] = p1;
	quad[2] = p2;

	StartCoroutine(CheckMoves());
	yield return null;
}

IEnumerator CheckMoves()	{
	top = new Vector3[2];
	foreach(Vector3 v in quad)	{
		if(v == new Vector3())
			continue;

		if(Mathf.FloorToInt(v.y) == Mathf.FloorToInt(top[0].y) && v.y > top[1].y)	{
			top[1] = top[0];
			top[0] = v;
		} else if(v.y > top[0].y)	{
			top[1] = top[0];
			top[0] = v;
		}
	}

	if(Vector3.Distance(transform.position, (top[0]+top[1])/2) <= reach)	{  //This code block doesn't always work!
		if(!Physics.CheckCapsule((top[0]+top[1])/2, (top[0]+top[1])/2+Vector3.up*2, 0.5f) && !Physics.Raycast((top[0]+top[1])/2+Vector3.up*2, Vector3.down, 1.95f))	{
			if(Physics.Raycast((top[0]+top[1])/2+Vector3.up*2+transform.forward/2, Vector3.down, 2.1f))	{
				transform.position = (top[0]+top[1])/2+Vector3.up*2;
			}
		}
	}

	yield return null;
}
}

First it gets the triangle that the players controllercollider hits. Then it Tries to figure out which vertex is the needed vertex to find the adjacent triangle and thus the whole quad. Now that its sure there is a level top edge, it finds the highest edge. Then it checks if the standard unity FPS controller can fit ontop of said edge! That last part is buggy but it works okay. Try it for yourself!

The big thing to do next is get all edges within reach, and not just the ones that are part of the quad hit by ControllerColliderHit. Also your mesh HAS to be read/write enabled I think!

This script actually works wonders C:, Thanks for sharing. I will edit it to suit my needs & send you copies of it :D. Thanks for your help.

Put invisible trigger colliders

Thanks for the suggestion but my meshes are procedural so would not be possible. This would work otherwise.

So just procedurally-generate the colliders as you procedurally-generate the mesh...

The only thing is to generate the colliders i need to know where the edge/ledge/surface is & that's why i'm asking this question. Also, i'm not asking how to add colliders or anything to do with colliders, i'm asking how to find the edge/surface/ledge of a mesh.

And I'm suggesting that, if your mesh is generated procedurally, the edges can be determined entirely as a product of the procedure, no?

What is your starting data structure/description of the mesh?