khanat-opennel-code/code/nel/src/pacs/edge_collide.cpp
2010-08-20 13:27:17 +02:00

684 lines
19 KiB
C++

// NeL - MMORPG Framework <http://dev.ryzom.com/projects/nel/>
// Copyright (C) 2010 Winch Gate Property Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "stdpacs.h"
#include "edge_collide.h"
using namespace NLMISC;
using namespace std;
namespace NLPACS
{
static const float EdgeCollideEpsilon= 1e-5f;
// ***************************************************************************
void CEdgeCollide::make(const CVector2f &p0, const CVector2f &p1)
{
P0= p0;
P1= p1;
// translation axis of the edge.
Dir= P1-P0;
Dir.normalize();
A0= P0*Dir;
A1= P1*Dir;
// line equation.
Norm.x= Dir.y;
Norm.y= -Dir.x;
C= - P0*Norm;
}
// ***************************************************************************
CRational64 CEdgeCollide::testPointMove(const CVector2f &start, const CVector2f &end, TPointMoveProblem &moveBug)
{
/*
To have a correct test (with no float precision problem):
- test first if there is collision beetween the 2 edges:
- test if first edge collide the other line.
- test if second edge collide the other line.
- if both true, yes, there is a collision.
- compute time of collision.
*/
// *this must be a correct edge.
if(P0==P1)
{
moveBug= EdgeNull;
return -1;
}
// if no movement, no collision.
if(start==end)
return 1;
// NB those edges are snapped (1/256 for edgeCollide, and 1/1024 for start/end), so no float problem here.
// precision is 20 bits.
CVector2f normEdge;
CVector2f normMove;
CVector2f deltaEdge;
CVector2f deltaMove;
// compute normal of the edge (not normalized, because no need, and for precision problem).
deltaEdge= P1-P0;
normEdge.x= -deltaEdge.y;
normEdge.y= deltaEdge.x;
// compute normal of the movement (not normalized, because no need, and for precision problem).
deltaMove= end-start;
normMove.x= -deltaMove.y;
normMove.y= deltaMove.x;
// distance from points of movment against edge line. Use double, because of multiplication.
// precision is now 43 bits.
double moveD0= (double)normEdge.x*(double)(start.x-P0.x) + (double)normEdge.y*(double)(start.y-P0.y);
double moveD1= (double)normEdge.x*(double)(end.x -P0.x) + (double)normEdge.y*(double)(end.y -P0.y);
// distance from points of edge against movement line. Use double, because of multiplication.
// precision is now 43 bits.
double edgeD0= (double)normMove.x*(double)(P0.x-start.x) + (double)normMove.y*(double)(P0.y-start.y);
double edgeD1= (double)normMove.x*(double)(P1.x-start.x) + (double)normMove.y*(double)(P1.y-start.y);
// If both edges intersect lines (including endpoints), there is a collision, else none.
sint sgnMove0, sgnMove1;
sgnMove0= fsgn(moveD0);
sgnMove1= fsgn(moveD1);
// special case if the 2 edges lies on the same line.
if(sgnMove0==0 && sgnMove1==0)
{
// must test if there is a collision. if yes, problem.
// project all the points on the line of the edge.
// Use double because of multiplication. precision is now 43 bits.
double moveA0= (double)deltaEdge.x*(double)(start.x-P0.x) + (double)deltaEdge.y*(double)(start.y-P0.y);
double moveA1= (double)deltaEdge.x*(double)(end.x -P0.x) + (double)deltaEdge.y*(double)(end.y -P0.y);
double edgeA0= 0;
double edgeA1= (double)deltaEdge.x*(double)deltaEdge.x + (double)deltaEdge.y*(double)deltaEdge.y;
// Test is there is intersection (endpoints included). if yes, return -1. else return 1 (no collision at all).
if(moveA1>=edgeA0 && edgeA1>=moveA0)
{
moveBug= ParallelEdges;
return -1;
}
else
return 1;
}
// if on same side of the line=> there is no collision.
if( sgnMove0==sgnMove1)
return 1;
// test edge against move line.
sint sgnEdge0, sgnEdge1;
sgnEdge0= fsgn(edgeD0);
sgnEdge1= fsgn(edgeD1);
// should not have this case, because tested before with (sgnMove==0 && sgnMove1==0).
nlassert(sgnEdge0!=0 || sgnEdge1!=0);
// if on same side of the line, no collision against this edge.
if( sgnEdge0==sgnEdge1 )
return 1;
// Here the edges intersect, but ensure that there is no limit problem.
if(sgnEdge0==0 || sgnEdge1==0)
{
moveBug= TraverseEndPoint;
return -1;
}
else if(sgnMove1==0)
{
moveBug= StopOnEdge;
return -1;
}
else if(sgnMove0==0)
{
// this should not arrive.
moveBug= StartOnEdge;
return -1;
}
// Here, there is a normal collision, just compute it.
// Because of Division, there is a precision lost in double. So compute a CRational64.
// First, compute numerator and denominator in the highest precision. this is 1024*1024 because of prec multiplication.
double numerator= (0-moveD0)*1024*1024;
double denominator= (moveD1-moveD0)*1024*1024;
sint64 numeratorInt= (sint64)numerator;
sint64 denominatorInt= (sint64)denominator;
/*
nlassert(numerator == numeratorInt);
nlassert(denominator == denominatorInt);
*/
/*
if (numerator != numeratorInt)
nlwarning("numerator(%f) != numeratorInt(%"NL_I64"d)", numerator, numeratorInt);
if (denominator != denominatorInt)
nlwarning("denominator(%f) != denominatorInt(%"NL_I64"d)", denominator, denominatorInt);
*/
return CRational64(numeratorInt, denominatorInt);
}
// ***************************************************************************
static inline float testCirclePoint(const CVector2f &start, const CVector2f &delta, float radius, const CVector2f &point)
{
// factors of the qaudratic: at^2 + bt + c=0
float a,b,c;
float dta;
float r0, r1, res;
CVector2f relC, relV;
// As long as delta is not NULL (ensured in testCircleMove() ), this code should not generate Divide by 0.
// compute quadratic..
relC= start-point;
relV= delta;
a= relV.x*relV.x + relV.y*relV.y;
// a should be >0. BUT BECAUSE OF PRECISION PROBLEM, it may be ==0, and then cause
// divide by zero (because b may be near 0, but not 0)
if(a==0)
{
// in this case the move is very small. return 0 if the point is in the circle and if we go toward the point
if(relC.norm()<radius && relC*delta<0)
return 0;
else
return 1;
}
else
{
b= 2* (relC.x*relV.x + relC.y*relV.y);
c= relC.x*relC.x + relC.y*relC.y - radius*radius;
// compute delta of the quadratic.
dta= b*b - 4*a*c; // b^2-4ac
if(dta>=0)
{
dta= (float)sqrt(dta);
r0= (-b -dta)/(2*a);
r1= (-b +dta)/(2*a);
// since a>0, r0<=r1.
if(r0>r1)
swap(r0,r1);
// if r1 is negative, then we are out and go away from this point. OK.
if(r1<=0)
{
res= 1;
}
// if r0 is positive, then we may collide this point.
else if(r0>=0)
{
res= min(1.f, r0);
}
else // r0<0 && r1>0. the point is already in the sphere!!
{
//nlinfo("COL: Point problem: %.2f, %.2f. b=%.2f", r0, r1, b);
// we allow the movement only if we go away from this point.
// this is true if the derivative at t=0 is >=0 (because a>0).
if(b>0)
res= 1; // go out.
else
res=0;
}
}
else
{
// never hit this point along this movement.
res= 1;
}
}
return res;
}
// ***************************************************************************
float CEdgeCollide::testCircleMove(const CVector2f &start, const CVector2f &delta, float radius, CVector2f &normal)
{
// If the movement is NULL, return 1 (no collision!)
if( delta.isNull() )
return 1;
// distance from point to line.
double dist= start*Norm + C;
// projection of speed on normal.
double speed= delta*Norm;
// test if the movement is against the line or not.
bool sensPos= dist>0;
bool sensSpeed= speed>0;
// Does the point intersect the line?
dist= fabs(dist) - radius;
speed= fabs(speed);
if( dist > speed )
return 1;
// if not already in collision with the line, test when it collides.
// ===============================
if(dist>=0)
{
// if signs are equals, same side of the line, so we allow the circle to leave the line.
if(sensPos==sensSpeed )
return 1;
// if speed is 0, it means that movement is parralel to the line => never collide.
if(speed==0)
return 1;
// collide the line, at what time.
double t= dist/speed;
// compute the collision position of the Circle on the edge.
// this gives the center of the sphere at the collision point.
CVector2d proj= CVector2d(start) + CVector2d(delta)*t;
// must add radius vector.
proj+= Norm * (sensSpeed?radius:-radius);
// compute projection on edge.
double aProj= proj*Dir;
// if on the interval of the edge.
if( aProj>=A0 && aProj<=A1)
{
// collision occurs on interior of the edge. the normal to return is +- Norm.
if(sensPos) // if algebric distance of start position was >0.
normal= Norm;
else
normal= -Norm;
// return time of collision.
return (float)t;
}
}
// else, must test if circle collide segment at t=0. if yes, return 0.
// ===============================
else
{
// There is just need to test if projection of circle's center onto the line hit the segment.
// This is not a good test to know if a circle intersect a segment, but other cases are
// managed with test of endPoints of the segment after.
float aProj= start*Dir;
// if on the interval of the edge.
if( aProj>=A0 && aProj<=A1)
{
// if signs are equals, same side of the line, so we allow the circle to leave the edge.
/* Special case: do not allow to leave the edge if we are too much in the edge.
It is important for CGlobalRetriever::testCollisionWithCollisionChains() because of the
"SURFACEMOVE NOT DETECTED" Problem.
Suppose we can walk on this chain SA/SB (separate Surface A/SurfaceB). Suppose we are near this edge,
and on Surface SA, and suppose there is another chain SB/SC the circle collide with. If we
return 1 (no collision), SB/SC won't be detected (because only SA/?? chains will be tested) and
so the cylinder will penetrate SB/SC...
This case arise at best if chains SA/SB and chain SB/SC do an angle of 45deg
*/
if(sensPos==sensSpeed && (-dist)<0.5*radius)
{
return 1;
}
else
{
// hit the interior of the edge, and sensPos!=sensSpeed. So must stop now!!
// collision occurs on interior of the edge. the normal to return is +- Norm.
if(sensPos) // if algebric distance of start position was >0.
normal= Norm;
else
normal= -Norm;
return 0;
}
}
}
// In this case, the Circle do not hit the edge on the interior, but may hit on borders.
// ===============================
// Then, we must compute collision sphere-points.
float tmin, ttmp;
// first point.
tmin= testCirclePoint(start, delta, radius, P0);
// second point.
ttmp= testCirclePoint(start, delta, radius, P1);
tmin= min(tmin, ttmp);
// if collision occurs, compute normal of collision.
if(tmin<1)
{
// to which point we collide?
CVector2f colPoint= tmin==ttmp? P1 : P0;
// compute position of the entity at collision.
CVector2f colPos= start + delta*tmin;
// and so we have this normal (the perpendicular of the tangent at this point).
normal= colPos - colPoint;
normal.normalize();
}
return tmin;
}
// ***************************************************************************
bool CEdgeCollide::testEdgeMove(const CVector2f &q0, const CVector2f &q1, const CVector2f &delta, float &tMin, float &tMax, bool &normalOnBox)
{
double a,b,cv,cc, d,e,f;
CVector2d tmp;
// compute D1 line equation of q0q1. bx - ay + c(t)=0, where c is function of time [0,1].
// ===========================
tmp= q1 - q0; // NB: along time, the direction doesn't change.
// Divide by norm()^2, so that a projection on this edge is true if the proj is in interval [0,1].
tmp/= tmp.sqrnorm();
a= tmp.x;
b= tmp.y;
// c= - q0(t)*CVector2d(b,-a). but since q0(t) is a function of time t (q0+delta*t), compute cv, and cc.
// so c= cv*t + cc.
cv= - CVector2d(b,-a)*delta;
cc= - CVector2d(b,-a)*q0;
// compute D2 line equation of P0P1. ex - dy + f=0.
// ===========================
tmp= P1 - P0;
// Divide by norm()^2, so that a projection on this edge is true if the proj is in interval [0,1].
tmp/= tmp.sqrnorm();
d= tmp.x;
e= tmp.y;
f= - CVector2d(e,-d)*P0;
// Solve system.
// ===========================
/*
Compute the intersection I of 2 lines across time.
We have the system:
bx - ay + c(t)=0
ex - dy + f=0
which solve for:
det= ae-bd (0 <=> // lines)
x(t)= (d*c(t) - fa) / det
y(t)= (e*c(t) - fb) / det
*/
// determinant of matrix2x2.
double det= a*e - b*d;
// if to near of 0. (take delta for reference of test).
if(det==0 || fabs(det)<delta.norm()*EdgeCollideEpsilon)
return false;
// intersection I(t)= pInt + vInt*t.
CVector2d pInt, vInt;
pInt.x= ( d*cc - f*a ) / det;
pInt.y= ( e*cc - f*b ) / det;
vInt.x= ( d*cv ) / det;
vInt.y= ( e*cv ) / det;
// Project Intersection.
// ===========================
/*
Now, we project x,y onto each line D1 and D2, which gives u(t) and v(t), each one giving the parameter of
the parametric line function. When it is in [0,1], we are on the edge.
u(t)= (I(t)-q0(t)) * CVector2d(a,b) = uc + uv*t
v(t)= (I(t)-P0) * CVector2d(d,e) = vc + vv*t
*/
double uc, uv;
double vc, vv;
// NB: q0(t)= q0+delta*t
uc= (pInt-q0) * CVector2d(a,b);
uv= (vInt-delta) * CVector2d(a,b);
vc= (pInt-P0) * CVector2d(d,e);
vv= (vInt) * CVector2d(d,e);
// Compute intervals.
// ===========================
/*
Now, for each edge, compute time interval where parameter is in [0,1]. If intervals overlap, there is a collision.
*/
double tu0, tu1, tv0, tv1;
// infinite interval.
bool allU=false, allV=false;
// compute time interval for u(t).
if(uv==0 || fabs(uv)<EdgeCollideEpsilon)
{
// The intersection does not move along D1. Always projected on u(t)=uc. so if in [0,1], OK, else never collide.
if(uc<0 || uc>1)
return false;
// else suppose "always valid".
tu0 =tu1= 0;
allU= true;
}
else
{
tu0= (0-uc)/uv; // t for u(t)=0
tu1= (1-uc)/uv; // t for u(t)=1
}
// compute time interval for v(t).
if(vv==0 || fabs(vv)<EdgeCollideEpsilon)
{
// The intersection does not move along D2. Always projected on v(t)=vc. so if in [0,1], OK, else never collide.
if(vc<0 || vc>1)
return false;
// else suppose "always valid".
tv0 =tv1= 0;
allV= true;
}
else
{
tv0= (0-vc)/vv; // t for v(t)=0
tv1= (1-vc)/vv; // t for v(t)=1
}
// clip intervals.
// ===========================
// order time interval.
if(tu0>tu1)
swap(tu0, tu1); // now, [tu0, tu1] represent the time interval where line D2 hit the edge D1.
if(tv0>tv1)
swap(tv0, tv1); // now, [tv0, tv1] represent the time interval where line D1 hit the edge D2.
normalOnBox= false;
if(!allU && !allV)
{
// if intervals do not overlap, no collision.
if(tu0>tv1 || tv0>tu1)
return false;
else
{
// compute intersection of intervals.
tMin= (float)max(tu0, tv0);
tMax= (float)min(tu1, tv1);
// if collision of edgeCollide against the bbox.
if(tv0>tu0)
normalOnBox= true;
}
}
else if(allU)
{
// intersection of Infinite and V interval.
tMin= (float)tv0;
tMax= (float)tv1;
// if collision of edgeCollide against the bbox.
normalOnBox= true;
}
else if(allV)
{
// intersection of Infinite and U interval.
tMin= (float)tu0;
tMax= (float)tu1;
}
else
{
// if allU && allV, this means delta is near 0, and so there is always collision.
tMin= -1000;
tMax= 1000;
}
return true;
}
// ***************************************************************************
float CEdgeCollide::testBBoxMove(const CVector2f &start, const CVector2f &delta, const CVector2f bbox[4], CVector2f &normal)
{
// distance from center to line.
float dist= start*Norm + C;
// test if the movement is against the line or not.
bool sensPos= dist>0;
// if signs are equals, same side of the line, so we allow the circle to leave the line.
/*if(sensPos==sensSpeed)
return 1;*/
// Else, do 4 test edge/edge, and return Tmin.
float tMin = 0.f, tMax = 0.f;
bool edgeCollided= false;
bool normalOnBox= false;
CVector2f boxNormal(0.f, 0.f);
for(sint i=0;i<4;i++)
{
float t0, t1;
bool nob;
CVector2f a= bbox[i];
CVector2f b= bbox[(i+1)&3];
// test move against this edge.
if(testEdgeMove(a, b, delta, t0, t1, nob))
{
if(edgeCollided)
{
tMin= min(t0, tMin);
tMax= max(t1, tMax);
}
else
{
edgeCollided= true;
tMin= t0;
tMax= t1;
}
// get normal of box against we collide.
if(tMin==t0)
{
normalOnBox= nob;
if(nob)
{
CVector2f dab;
// bbox must be CCW.
dab= b-a;
// the normal is computed so that the vector goes In the bbox.
boxNormal.x= -dab.y;
boxNormal.y= dab.x;
}
}
}
}
// if collision occurs,and int the [0,1] interval...
if(edgeCollided && tMin<1 && tMax>0)
{
// compute normal of collision.
if(normalOnBox)
{
// assume collsion is an endpoint of the edge against the bbox.
normal= boxNormal;
}
else
{
// assume collision occurs on interior of the edge. the normal to return is +- Norm.
if(sensPos) // if algebric distance of start position was >0.
normal= Norm;
else
normal= -Norm;
}
// compute time of collison.
if(tMin>0)
// return time of collision.
return tMin;
else
{
// The bbox is inside the edge, at t==0. test if it goes out or not.
// accept only if we are much near the exit than the enter.
/* NB: 0.2 is an empirical value "which works well". Normally, 1 is the good value, but because of the
"SURFACEMOVE NOT DETECTED" Problem (see testCircleMove()), we must be more restrictive.
*/
if( tMax<0.2*(-tMin) )
return 1;
else
return 0;
}
}
else
return 1;
}
// ***************************************************************************
bool CEdgeCollide::testBBoxCollide(const CVector2f bbox[4])
{
// clip the edge against the edge of the bbox.
CVector2f p0= P0, p1= P1;
for(sint i=0; i<4; i++)
{
CVector2f a= bbox[i];
CVector2f b= bbox[(i+1)&3];
CVector2f delta= b-a, norm;
// sign is important. bbox is CCW. normal goes OUT the bbox.
norm.x= delta.y;
norm.y= -delta.x;
float d0= (p0-a)*norm;
float d1= (p1-a)*norm;
// if boths points are out this plane, no collision.
if( d0>0 && d1>0)
return false;
// if difference, must clip.
if( d0>0 || d1>0)
{
CVector2f intersect= p0 + (p1-p0)* ((0-d0)/(d1-d0));
if(d1>0)
p1= intersect;
else
p0= intersect;
}
}
// if a segment is still in the bbox, collision occurs.
return true;
}
} // NLPACS