Skip to main content
GameDev.net gamedev.net
🔒 Locked

funnel algorithm

Started by redbaseone Jan 25, 2010 at 2:20 PM 8 replies 7.2k views
Original Post
redbaseone
redbaseone
Hello, I'm new to this subject and i hope you can help me. I've seen this document: http://www.cs.ualberta.ca/~mburo/ps/thesis_demyen_2006.pdf (see pages 52 - 61) I'm interested in how Funnel Algorithm works and i'm trying to understand this. So far I've succesfuly implemented A* search on a nav-mesh (3D triangles) and the path generated is correctly found. So far the "monster" in my application walks thru' this path from one center to next triangle's center - until goal is reached. I have the Common Edges between adiacent triangles in the A* Path. I have also the Start and the Goal 3D points (i.e the monster start position and respectively the destination where the monster sould arrive). Now, i know that implementing Funnel Algorithm requires first to get 2 lists: - list 1 = list with ALL the vertices on the left of channel (in A* path). - list 2 = list with ALL the vertices on the right of channel (in A* path). So I build these lists (channel1 & channel2) like so :


// returns in "p1" & "p2" (two 3D points)  =>   a common edge between two triangles (n1 & n2 are adiacent triangles in path)
void CFunnel::GetCommonEdge(CNavNode *n1, CNavNode *n2, CVector3 &p1, CVector3 &p2)
{
	int index = -1;
	CVector3 found[2];

	// loop thru all vertices in triangle 1
	for(int c2=0; c2<3; c2++)
	{
		// loop thru all vertices in triangle 2
		for(int c1=0; c1<3; c1++)
		{
			// if common vertex is found then store it to "found" variable .
			if(*n1->point[c2]==*n2->point[c1])
			{
				index++;
				found[index] = *n1->point[c2];
			}
			
		}
	}
	
        // p1 = first common point, p2 = second common point.
	p1 = found[0];
	p2 = found[1];

}

// 
void CFunnel::GetFunnel(CVector3 *start, CVector3 *goal)
{	
	// clear values in lists (channel1 & channel2 are lists of 3d points).
	channel1.clear();
	channel2.clear();
	
	// get number of triangles found with A* (i.e no triangles in the path).
	WORD size = AStar.m_ClosedList.size()-1;
	
	// loop thru' all triangles.
	for(unsigned short q=0; q<size; q++)
	{
		// get current triangle:
		CNavNode* current  = AStar.m_ClosedList[q];

		// get current's adiacent triangle:
		CNavNode* succesor = AStar.m_ClosedList[q+1];
		

		// 2 vertices ( a common edge has 2 vertices)
		CVector3 common[2];

		//get common edge between current & succesor triangles and store found common points to common[0] & common[1] 
                // line from common[0] to common[1] define now the common edge between "current" and "succesor" triangles.
		this->GetCommonEdge(current,succesor, common[0], common[1]);

				
		// 2D Crossproduct = returns if common[0] is in clocwise order defined in respect to common[1]. (ignoring y axis)
		float cross = common[0].x*common[1].z - common[0].z*common[1].x;
		
		// if clockwise 
		if(cross>0)
		{
			// add common[0] to channel1
			channel1.push_back(common[1]);
			// add common[1] to channel2
			channel2.push_back(common[0]);
		}
		
		else  // else counterclokwise
		{
			// add common[1] to channel1
			channel1.push_back(common[0]);
			// add common[0] to channel2
			channel2.push_back(common[1]);
		}

	}



}


here are the results (screen-shoot): and the incorrect one (swiched vertices): I get messed up results i think because of the GetCommonEdge() function and i dont know how to do it correctly. How can i build left & right channels correctly ? (i.e the channel1 and channel2) What's the next step implementing Funnel Algorithm ?
Zakwayda
Zakwayda
It looks to me like the problem is that your method of finding the common edge is not sensitive to edge direction.

Each edge that is processed needs to be oriented consistently with respect to the direction of traversal. Imagine an agent crossing an edge from triangle A to triangle B, and assume that the triangles are wound CCW when viewed from above. When viewed from the point of view of the agent, the first endpoint of the crossed edge should always be on the right, and the second endpoint should always be on the left. (For CW-wound triangles it would be the opposite.)

Your algorithm to find the common edge is really finding common points, not edges, and it looks to me like these points may be returned in arbitrary order. What you instead want to do is to compare each directed edge of triangle A with each directed edge of triangle B and find the pair that matches (the two edges match if the first endpoint of the first edge is equivalent to the second endpoint of the second edge and vice versa). Then, add the edge corresponding to triangle A in the order 'first endpoint, second endpoint'.
jacksaccountongamedev
jacksaccountongamedev
Quote:
Original post by redbaseone
// 2D Crossproduct = returns if common[0] is in clocwise order defined in respect to common[1]. (ignoring y axis)
float cross = common[0].x*common[1].z - common[0].z*common[1].x;



Hi Redbase one. I'm not sure what is going on here. I think I understand what you're trying to do, but two points isn't enough to determine if one is 'on the left or right.' How can two points have a winding order? It's not enough information. You need a third point to act as a reference.

In any case, you really only need to make a directional comparison once, at the start. The rest of the funnel arrangement can easily be worked out on the basis that if we know that one point on a edge is on one side, the other point is clearly on the opposite side. Thus, the whole algorithm can be as follows:

Add the first left point, easily found using the starting point as a anchor, to the left list. Then cycle through each triangle, starting with the second one. For each triangle, there will be two shared points with the previous triangle. One of those points will be either be the very front of the left list or the front of the right list. Add the other point to the front of the opposite list, and move to the next triangle.

As you can see, it actually breaks down to a very simple procedure. If you're storing left and right points on one 'annotated' list, then you'll need to use two extra values to keep track of what the most recently added left and right points are.
redbaseone
redbaseone
Thank you for your replies and pointing me in right direction.
I think I understand what you explineded earlier to me (must code it now)
and i'll give it a try and hope i'll get to the bottom of this. :)

thanks again.
redbaseone
redbaseone
OK, all done , I think it is ok now.
Here is the code corected:

// returns in "p1" & "p2"   =>   a common edge between two triangles.void CFunnel::GetCommonEdge(CNavNode *n1, CNavNode *n2, CVector3 &p1, CVector3 &p2){	int index = -1;	CVector3 found[2];	// loop thru all vertices in triangle 1	for(int c2=0; c2<3; c2++)	{		// loop thru all vertices in triangle 2		for(int c1=0; c1<3; c1++)		{			// if common vertex is found then store it to "found" variable.			if(*n1->point[c2]==*n2->point[c1])			{				index++;				found[index] = *n1->point[c2];			}					}	}		p1 = found[0];	p2 = found[1];}// void CFunnel::GetFunnel(CVector3 *start, CVector3 *goal){		// clear values in lists (channel1 & channel2 are lists of 3d points).	left.clear();	right.clear();		// two point variables	CVector3 p1, p2;	// get common edge between triangle[0] and triangle[1] -> the first two (adiacent) triangles in path.	// result is stored in p1 & p2	this->GetCommonEdge(AStar.m_ClosedList[0], AStar.m_ClosedList[1], p1, p2);	// add goal node to both vector lists	this->left.push_back(*goal+CVector3(0,-26.7f,0));	this->right.push_back(*goal+CVector3(0,-26.7f,0));		// push to left & right points	this->left.push_back(p1);	this->right.push_back(p2);		// loop thru every triangle in path starting with triangle 1 (not 0)	for(unsigned int i=1; i<AStar.m_ClosedList.size()-1; i++)	{		// get current triangle		CNavNode* n1 = AStar.m_ClosedList;		// get current's next adiacent triangle		CNavNode* n2 = AStar.m_ClosedList[i+1];		// get common edge between n1 & n2		this->GetCommonEdge(n1, n2, p1, p2);			//  check p1 & p2 points with left & right vector lists -> and push to opposite list		// TODO: must NOT add point twice to vector list :)		if(right.back()==p1) left.push_back(p2);		if(right.back()==p2) left.push_back(p1);		if(left.back()==p1) right.push_back(p2);		if(left.back()==p2) right.push_back(p1);	}		// finnaly add also starting point to both vector lists.	this->left.push_back(*start+CVector3(0,-26.7f,0));	this->right.push_back(*start+CVector3(0,-26.7f,0));}





and a picture with above code:



seems that it is now generated corectly.
thank you again !
jacksaccountongamedev
jacksaccountongamedev
Quote:
Original post by redbaseone
// get common edge between triangle[0] and triangle[1] -> the first two (adiacent) triangles in path.
// result is stored in p1 & p2
this->GetCommonEdge(AStar.m_ClosedList[0], AStar.m_ClosedList[1], p1, p2);

// add goal node to both vector lists
this->left.push_back(*goal+CVector3(0,-26.7f,0));
this->right.push_back(*goal+CVector3(0,-26.7f,0));

// push to left & right points
this->left.push_back(p1);
this->right.push_back(p2);


Hi Redbaseone.

You don't seem to be making a comparison with the initial two points in order to determine which one is for the left and which is for the right. Your lists will still come out consistent, but will be flipped around fifty percent of the time (ie left points stored in the right list and vice-a-versa).

Quote:

// check p1 & p2 points with left & right vector lists -> and push to opposite list
// TODO: must NOT add point twice to vector list :)
if(right.back()==p1) left.push_back(p2);
if(right.back()==p2) left.push_back(p1);

if(left.back()==p1) right.push_back(p2);
if(left.back()==p2) right.push_back(p1)


These cases should be mutually exclusive. Otherwise, you're adding the same points several times. For example, in any case where the first conditional statement is met, adding the point to the left list will cause the last conditional statement to be true as well. It should really be:

if(right.back()==p1) left.push_back(p2);else if(right.back()==p2) left.push_back(p1);else if(left.back()==p1) right.push_back(p2);else if(left.back()==p2) right.push_back(p1)


Also, just for clarity for any future readers, I noticed that you're adding both initial points, and then looking forward to the next triangle as you iterate through. This is essentially the same as adding one of the initial points and then looking backwards at the previous triangle in the loop.

Glad to have helped!
redbaseone
redbaseone
thanks all for replying.

fixed that jack_1313.
it seems that indeed my code fliped sides. :) at first didn't noticed that.

i had little time in last days ... thanks again.
redbaseone
redbaseone
Well, i'm stuck with Funnel Algorithm, and dont even
know if i'm doing this right so far ... please help !!

In my code - i have these variables :

"left" vector list = green lines in pictures.
"right" vector list = red lines in pictures.

"fRight" temporary vector list = cyan lines.
"fLeft" temporary vector list = yellow lines.

Code:

          // build funnel:         // -------------	// left & right = vectors wich contain ALL the points in the left & right of the channel 	if(left.empty() || right.empty()) return;	// fLeft & fRight = temporary vector lists wich will contain temporary vertices from left & right sides of the channel 	this->fLeft.clear();	this->fRight.clear();	// clear all in this vector & add first apex in vector list. 	this->m_Apex.clear();	this->m_Apex.push_back(*goal+CVector3(0,-26.7f,0));	// get all the points in the left side of the channel and all on the right.	const unsigned int RMAX = right.size();	const unsigned int LMAX = left.size();	// these are used to iterate thru fLeft & fRight ( starting from 0 by default, i.e first element)	unsigned int idxRight = 0;	unsigned int idxLeft = 0;	// while not reached end of list of vertices do : 	// ----	// TODO: i dont think this condition is ok here !	// ----	while( idxRight < RMAX && idxLeft < LMAX )	{			// add first point from to fRight			fRight.push_back(right[idxRight]);			// if more than 2 elements in fRight 			if(fRight.size()>2)			{				// iterator = last element in fRight.				vector<CVector3>::iterator it = fRight.end()-1;	                        				// loop thru all elements in fRight 				while(it > fRight.begin()+1)				{					// get last, last-1 and last-2 vertices (points) from fRight					CVector3 s1 = *(it);					CVector3 s2 = *(it-1);					CVector3 s3 = *(it-2);	 					// check that triangle (s1,s2,s3) is CCW 					if( s1.CCW( s1, s2, s3)==true )					{						// if on "inside" => remove s2 from fRight. (unifying - s1 with s3)						fRight.erase(it-1);						it = fRight.end()-1;					}					else 						// if on "outside" => just skip (last pushed point in fRight is valid and remains in fRight).						it--;				}	 			}					// add new vertice to left vector list			fLeft.push_back(left[idxLeft]);			// if more than 2 elements in fLeft			if(fLeft.size()>2)			{				// iterator = fLeft last element				vector<CVector3>::iterator it = fLeft.end()-1;	                        				// loop thru fLeft elements starting from end 				while(it > fLeft.begin()+1)				{					// get last, last-1 and last-2 vertices (points) from fLeft					CVector3 s1 = *(it);					CVector3 s2 = *(it-1);					CVector3 s3 = *(it-2);	 					// check that triangle (s1,s2,s3) is CW 					if( s1.CW( s1, s2, s3)==true )					{						// if on "outside" remove s2 from fList.						fLeft.erase(it-1);						it = fLeft.end()-1;					}					else 						// if on "inside" just skip (last pushed point in fLeft is valid and remains in fList).						it--;				}	 			}                   // TODO : check here for new apex ??			        		// increment indices (fLeft & fRight indices)		idxRight++;		idxLeft++;							} 


Here are the pictures made with above code :















The Question i have:

1) Have i done it correctly so far ? (building yellow and cyan lines) ?


2) It seems that the left (yellow) channel is build correctly.
Is it suppose to be correct that cyan line overlaps yellow line ??
( at current loop say ... "idxLeft = 5" and "idxRight=5" ?? )


3) How can i add new APEX ? Wich is the condition ?
I've tryied :

// check if points (fRight.back, fLeft.back, Apex) are CCW !
if( CCW( fRight.back() , fLeft.back(), m_Apex.back())==true)
{
// add new apex if so.
m_Apex.push_back( fRight.back() );
}
and didn't worked ! :(





Zakwayda
Zakwayda
This won't be an in-depth response, but I'll just go ahead and quickly mention a couple of things.

First of all, it looks like in each of your example images, one or the other of the secondary line lists traces out the correct path, so you must be doing something right :)

That said, I'm not sure what the significance of the separate 'left' and 'right' lists is (either for the funnel sides or for the path). Typically, you would store the funnel in a single data structure, ideally a double-ended queue. This is important both conceptually and with respect to an implementation of the algorithm, since when popping points off of the funnel you should be able to 'cross over' the point corresponding to the current apex without any special consideration. In other words, you shouldn't have to 'switch lists' when this happens; rather, it should be handled transparently.

The layout of the container holding the funnel points should look something like this (L and R are left and right, A is the apex):
...L4...L3...L2...L1...A...R1...R2...R3...R4...
Note that this is a single container (rather than a pair of containers), and that the position of A is arbitrary (it can occupy any slot in the container, and any elements to the left of it will always be the 'left side of the funnel', and any elements to the right of it will always be the 'right side of the funnel').

IMO, the funnel algorithm is a difficult algorithm to implement via guesswork or trial and error - it's really easier if you first gain a solid understanding of what the algorithm is actually doing, after which implementing it will become more straightforward. If you're still having trouble with this, one thing you might try is to draw out a few example cases with pen and paper and then walk through the steps one by one (be sure to try it with a configuration that doesn't have a clear line of sight from the start point to the goal point). While walking through the algorithm, it should be fairly clear visually what needs to happen at each step, which in turn should make it easier to translate the algorithm into working code.
redbaseone
redbaseone
I think now is working.
Here is the code so far (updated and fixed some stuff):

 void CFunnel::GetFunnel(CVector3 *start, CVector3 *goal){		// clear values in lists (channel1 & channel2 are lists of 3d points).	left.clear();	right.clear();	m_Apex.clear();	fLeft.clear();	fRight.clear();	MyStart = *start + CVector3(0,-27.6f,0);	MyGoal = *goal+ CVector3(0,-27.6f,0);	// two point variables	CVector3 p1, p2;	// get common edge between triangle[0] and triangle[1] -> the first two (adiacent) triangles in path.	// result is stored in p1 & p2	this->GetCommonEdge(AStar.m_ClosedList[0], AStar.m_ClosedList[1], p1, p2);	// add goal node to both vector lists	this->left.push_back(*goal+CVector3(0,-27.6f,0));	this->right.push_back(*goal+CVector3(0,-27.6f,0));	this->m_Apex.push_back(*goal+CVector3(0,-27.6f,0));	// push to left or right points if clockwise/counter clockwise in respect to goal.	if(p1.CCW(p1,p2,*goal))		{		left.push_back(p1);		right.push_back(p2);	}	else	{		left.push_back(p2);		right.push_back(p1);	}	this->fLeft.push_back(left.back());	this->fRight.push_back(right.back());	// loop thru every triangle in path starting with triangle 2 	for(unsigned int i=2; i<AStar.m_ClosedList.size(); i++)	{		CVector3 LeftLast = fLeft.back();		CVector3 RightLast = fRight.back();		// get current triangle		CNavNode* n1 = AStar.m_ClosedList[i-1];		// get current's next adiacent triangle		CNavNode* n2 = AStar.m_ClosedList;		// get common edge between n1 & n2		this->GetCommonEdge(n1, n2, p1, p2);		//  check p1 & p2 points with left & right vector lists -> and push to opposite list		if(right.back()==p1) left.push_back(p2);		else if(right.back()==p2) left.push_back(p1);		else if(left.back()==p1) right.push_back(p2);		else if(left.back()==p2) right.push_back(p1);		CVector3 LeftFirst = left.back();		CVector3 RightFirst = right.back();		// ----------------------------------------		// left wall (funnel)		// ----------------------------------------		if(p1.CCW(LeftFirst,LeftLast,m_Apex.back()))		{			// adding "outside" point to left wall			if(fLeft.back()!=LeftFirst) fLeft.push_back(LeftFirst);		}		else		{			// adding "inside" point to left wall			fLeft.push_back(LeftFirst);			vector<CVector3>::iterator it = fLeft.begin();			while(it<fLeft.end()-1)			{				if(p1.CW(LeftFirst,*it,m_Apex.back()))				{					fLeft.erase(it);					it = fLeft.begin();				}				else it++;							}		}		// ----------------------------------------		// right wall (funnel)		// ----------------------------------------		if(p1.CW(RightFirst,RightLast,m_Apex.back()))		{			// adding "outside" point to right wall			if(fRight.back()!=RightFirst) fRight.push_back(RightFirst);		}		else		{			// adding "inside" point to right wall			fRight.push_back(RightFirst);			vector<CVector3>::iterator it = fRight.begin();			while(it<fRight.end()-1)			{				if(p1.CCW(RightFirst,*it,m_Apex.back()))				{					fRight.erase(it);					it = fRight.begin();				}				else it++;							}		}		// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@		// processing left side apex-es first, else processing right side apex-es.		// @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@		if(p1.CCW(RightFirst, fLeft.front(), m_Apex.back()))		{			vector<CVector3>::iterator it = fLeft.begin();						while(it<fLeft.end()-1)			{				if(p1.CCW(RightFirst,*it, m_Apex.back())) 				{					m_Apex.push_back(*it);					fLeft.erase(it);					it = fLeft.begin();				}				else					it++;			}		}		else		if(p1.CW(LeftFirst, fRight.front(), m_Apex.back()))		{			vector<CVector3>::iterator it = fRight.begin();			while(it<fRight.end()-1)			{				if(p1.CW(LeftFirst,*it, m_Apex.back())) 				{					m_Apex.push_back(*it);					fRight.erase(it);					it = fRight.begin();									}				else					it++;			}		}	}			// finnaly add also starting point to both vector lists.	this->left.push_back(*start+CVector3(0,-27.6f,0));	this->right.push_back(*start+CVector3(0,-27.6f,0));	}


and a picture:



Legend:
- cyan lines = right channel in current funnel
- yellow line = left channel in current funnel
- red & right = left & right - contains points on left & right side
of the triangles
- brown lines = the Path.

I'm sure it's very buggy, but for now it works just fine !
Didn't used double-ended queue, anyway thanks for the tip jyk !
Ill use that soon because my code is a mess anyway !


-No matter the length of the path (brown) generated (and also look at the picture - left & right funnel as in picture - cyan and yellow lines), still not processing last part of this funnel. Must fix that !
-That's because, as you can see in the picture, after ONE more step ahead to goal -> the loop ends and no testing is done for last stage (when the yellow will overlap cyan lines and reach goal)

Any comments / tips will be apreciated :).
Thanks.

[Edited by - redbaseone on February 10, 2010 1:03:15 PM]

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.