三角形

在recastnavigation中,三角形是最基础的元素,很多逻辑都是基于三角形进行的,其中比较常见的一个操作就是计算指定点到某三角形上的最近距离。由于三角形通常代表行走面,而给定点P可能是场景中的任意位置,所以这个操作通常会用来计算可行走面的最近距离。

recastnavigation的计算

主体代码在dtClosestPtPointTriangle函数,代码风格一直简洁明快,但是注释也说明了关键思路和步骤。

void dtClosestPtPointTriangle(float* closest, const float* p,
const float* a, const float* b, const float* c)
{
// Check if P in vertex region outside A
float ab[3], ac[3], ap[3];
dtVsub(ab, b, a);
dtVsub(ac, c, a);
dtVsub(ap, p, a);
float d1 = dtVdot(ab, ap);
float d2 = dtVdot(ac, ap);
if (d1 <= 0.0f && d2 <= 0.0f)
{
// barycentric coordinates (1,0,0)
dtVcopy(closest, a);
return;
} // Check if P in vertex region outside B
float bp[3];
dtVsub(bp, p, b);
float d3 = dtVdot(ab, bp);
float d4 = dtVdot(ac, bp);
if (d3 >= 0.0f && d4 <= d3)
{
// barycentric coordinates (0,1,0)
dtVcopy(closest, b);
return;
} // Check if P in edge region of AB, if so return projection of P onto AB
float vc = d1*d4 - d3*d2;
if (vc <= 0.0f && d1 >= 0.0f && d3 <= 0.0f)
{
// barycentric coordinates (1-v,v,0)
float v = d1 / (d1 - d3);
closest[0] = a[0] + v * ab[0];
closest[1] = a[1] + v * ab[1];
closest[2] = a[2] + v * ab[2];
return;
} // Check if P in vertex region outside C
float cp[3];
dtVsub(cp, p, c);
float d5 = dtVdot(ab, cp);
float d6 = dtVdot(ac, cp);
if (d6 >= 0.0f && d5 <= d6)
{
// barycentric coordinates (0,0,1)
dtVcopy(closest, c);
return;
} // Check if P in edge region of AC, if so return projection of P onto AC
float vb = d5*d2 - d1*d6;
if (vb <= 0.0f && d2 >= 0.0f && d6 <= 0.0f)
{
// barycentric coordinates (1-w,0,w)
float w = d2 / (d2 - d6);
closest[0] = a[0] + w * ac[0];
closest[1] = a[1] + w * ac[1];
closest[2] = a[2] + w * ac[2];
return;
} // Check if P in edge region of BC, if so return projection of P onto BC
float va = d3*d6 - d5*d4;
if (va <= 0.0f && (d4 - d3) >= 0.0f && (d5 - d6) >= 0.0f)
{
// barycentric coordinates (0,1-w,w)
float w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
closest[0] = b[0] + w * (c[0] - b[0]);
closest[1] = b[1] + w * (c[1] - b[1]);
closest[2] = b[2] + w * (c[2] - b[2]);
return;
} // P inside face region. Compute Q through its barycentric coordinates (u,v,w)
float denom = 1.0f / (va + vb + vc);
float v = vb * denom;
float w = vc * denom;
closest[0] = a[0] + ab[0] * v + ac[0] * w;
closest[1] = a[1] + ab[1] * v + ac[1] * w;
closest[2] = a[2] + ab[2] * v + ac[2] * w;
}

为了便于理解,简单画了一个示意图,图中1——6个区域分别对应了函数中的6个返回点(前5个return和最后一个自然退出)。

d1 <= 0.0f && d2 <= 0.0f

点乘有一个优良的几何性质,点乘的结果表示了两个向量夹角的cos值,该结果的正负表示了两个向量之间的夹角是否大于90度。如果点乘结果小于0,表示两个向量夹角大于90度。

这里另个判断表示点P在AB向量反方向,并且也在AC向量反方向,所以对应的就是图中的区域①。

d3 >= 0.0f && d4 <= d3

这里的 d4 <= d3 看起来不太直观:这个地方应该是P应该在CB向量的正方向,也就是CB*PB >= 0。

这里只是使用了一个简单的向量等式CB = (AB - AC) ,所以CB * PB >=0 等价于 (AB - AC) * PB >= 0 等价于 AB * PB >= AC * PB。

由于

float d3 = dtVdot(ab, bp);

并且

float d4 = dtVdot(ac, bp);

所以 AB * PB >= AC * PB <==> d3 >= d4。

vc <= 0.0f

一个基于极坐标的解释,由于网站规模比较小,文章数量也不多,恐怕哪天就不见了,所以还是整段全部拷贝过来。同样的问题,还有一个pdf格式的描述,看起来更舒服些。

The advantage of the method above is that it's very simple to understand so that once you read it you should be able to remember it forever and code it up at any time without having to refer back to anything. It's just - hey the point has to be on the same side of each line as the triangle point that's not in the line. Cake.

Well, there's another method that is also as easy conceptually but executes faster. The downside is there's a little more math involved, but once you see it worked out it should be no problem.

So remember that the three points of the triangle define a plane in space. Pick one of the points and we can consider all other locations on the plane as relative to that point. Let's go with A -- it'll be our origin on the plane. Now what we need are basis vectors so we can give coordinate values to all the locations on the plane. We'll pick the two edges of the triangle that touch A, (C - A) and (B - A). Now we can get to any point on the plane just by starting at A and walking some distance along (C - A) and then from there walking some more in the direction (B - A).

With that in mind we can now describe any point on the plane as

    P = A + u * (C - A) + v * (B - A)
Notice now that if u or v < 0 then we've walked in the wrong direction and must be outside the triangle. Also if u or v > 1 then we've walked too far in a direction and are outside the triangle. Finally if u + v > 1 then we've crossed the edge BC again leaving the triangle. Given u and v we can easily calculate the point P with the above equation, but how can we go in the reverse direction and calculate u and v from a given point P? Time for some math! P = A + u * (C - A) + v * (B - A) // Original equation
(P - A) = u * (C - A) + v * (B - A) // Subtract A from both sides
v2 = u * v0 + v * v1 // Substitute v0, v1, v2 for less writing // We have two unknowns (u and v) so we need two equations to solve
// for them. Dot both sides by v0 to get one and dot both sides by
// v1 to get a second.
(v2) . v0 = (u * v0 + v * v1) . v0
(v2) . v1 = (u * v0 + v * v1) . v1 // Distribute v0 and v1
v2 . v0 = u * (v0 . v0) + v * (v1 . v0)
v2 . v1 = u * (v0 . v1) + v * (v1 . v1) // Now we have two equations and two unknowns and can solve one
// equation for one variable and substitute into the other. Or
// if you're lazy like me, fire up Mathematica and save yourself
// some handwriting.
Solve[v2.v0 == {u(v0.v0) + v(v1.v0), v2.v1 == u(v0.v1) + v(v1.v1)}, {u, v}]
u = ((v1.v1)(v2.v0)-(v1.v0)(v2.v1)) / ((v0.v0)(v1.v1) - (v0.v1)(v1.v0))
v = ((v0.v0)(v2.v1)-(v0.v1)(v2.v0)) / ((v0.v0)(v1.v1) - (v0.v1)(v1.v0))
Here's an implementation in Flash that you can play with. :)
// Compute vectors
v0 = C - A
v1 = B - A
v2 = P - A // Compute dot products
dot00 = dot(v0, v0)
dot01 = dot(v0, v1)
dot02 = dot(v0, v2)
dot11 = dot(v1, v1)
dot12 = dot(v1, v2) // Compute barycentric coordinates
invDenom = 1 / (dot00 * dot11 - dot01 * dot01)
u = (dot11 * dot02 - dot01 * dot12) * invDenom
v = (dot00 * dot12 - dot01 * dot02) * invDenom // Check if point is in triangle
return (u >= 0) && (v >= 0) && (u + v < 1)

但是无论如何,两者说明的内容都是和代码中的不完全相同,不过也只是格式不同,本质是一样的,遗憾的是我还是没办法理解这种实现的直观(intuitive)解释:-(。

想让recastnavigation和文档中的内容相同,只需要做简单的向量转换即可,把代码中的BP根据向量减法替换为(AP - AB),然后带入

const float vc = d1*d4 - d3*d2;

d1d4 - d3d2 = (ABAP) * (ACBP) - (AB * BP) * (AC * AP) = AB * AP (AC * (AP - AB)) - (AB * (AP - AB)) * AC * AP = AB * AP * AC * AP - AB * AP * AC * AB - AB * AP * AC * AP + AB * AB * AC * AP

由于第1项和第3项内容相同,符号相反,所以可以抵消掉,剩余内容为

-AB * AP * AC * AB + AB * AB * AC * AP = AB * AB * AC * AP - AB * AP * AC * AB = dot11 * dot02 - dot12 * dot01

由于点乘双方顺序无关,所以上面值和前面的u值相同。

根据u的定义,P点一定是在AC的反方向分量上(加上另一个ABv分量,无论这个ABv分量是多少,P一定在AB的外侧:因为P点是通过两个基向量AC和AB与U和v标量获得,也就是沿AC移动u然后再沿着AB移动v个距离),所以在AB的外侧

另外,embree库的closestPointTriangle函数实现,PhysX库也都使用了相同的方法。

UE的实现

UE的实现比较直观,就是计算垂线,然后判断方向,这个是最直观的实现方法,也最容易理解。


FVector FMath::ClosestPointOnTriangleToPoint(const FVector& Point, const FVector& A, const FVector& B, const FVector& C)
{
//Figure out what region the point is in and compare against that "point" or "edge"
const FVector BA = A - B;
const FVector AC = C - A;
const FVector CB = B - C;
const FVector TriNormal = BA ^ CB; // Get the planes that define this triangle
// edges BA, AC, BC with normals perpendicular to the edges facing outward
const FPlane Planes[3] = { FPlane(B, TriNormal ^ BA), FPlane(A, TriNormal ^ AC), FPlane(C, TriNormal ^ CB) };
int32 PlaneHalfspaceBitmask = 0; //Determine which side of each plane the test point exists
for (int32 i=0; i<3; i++)
{
if (Planes[i].PlaneDot(Point) > 0.0f)
{
PlaneHalfspaceBitmask |= (1 << i);
}
} FVector Result(Point.X, Point.Y, Point.Z);
switch (PlaneHalfspaceBitmask)
{
case 0: //000 Inside
return FVector::PointPlaneProject(Point, A, B, C);
case 1: //001 Segment BA
Result = FMath::ClosestPointOnSegment(Point, B, A);
break;
case 2: //010 Segment AC
Result = FMath::ClosestPointOnSegment(Point, A, C);
break;
case 3: //011 point A
return A;
case 4: //100 Segment BC
Result = FMath::ClosestPointOnSegment(Point, B, C);
break;
case 5: //101 point B
return B;
case 6: //110 point C
return C;
default:
UE_LOG(LogUnrealMath, Log, TEXT("Impossible result in FMath::ClosestPointOnTriangleToPoint"));
break;
} return Result;
}

最新文章

  1. Jsp九大内置对象及其作用域
  2. UESTC 880 生日礼物 --单调队列优化DP
  3. js:数据结构笔记13--检索算法
  4. [LeetCode]题解(python):059-Spiral Matrix II
  5. android于src和background差额
  6. html5图片上传时IOS和Android均显示摄像头拍照和图片选择
  7. ElasticSearch本地调测环境构建
  8. pm2 常用命令解析
  9. Python3学习之路~7.4 动态导入模块
  10. 个人练习:使用HTML+CSS3制作图片轮播功能(不使用JavaScript)
  11. &lt;Dare To Dream 团队&gt;第二次作业:基于B/S的家教管理系统
  12. 规范大于编码-我的javaWeb开发规范
  13. error: conflicting type qualifiers for &amp;#39;xxxxx&amp;#39;
  14. es6 generator 基础知识
  15. C语言 &#183; 实现strcmp函数 &#183; 字符串比较
  16. redis中的发布订阅(Pub/Sub)
  17. UVA11736_Debugging RAM
  18. hdu 4678
  19. 微信开发 api 需要 https 服务器
  20. nginx源码完全注释(1)ngx_alloc.h / ngx_alloc.c

热门文章

  1. Vicinity Vision Transformer概述
  2. [Unity]利用Mesh绘制简单的可被遮挡,可以探测的攻击指示器
  3. Prowide Core:SWIFT [tm] MT(FIN)消息传递的Java框架 开源
  4. ubuntu docker pull 失败 Error response from daemon: Get https://registry-1.docker.io/v2/
  5. 采集存储计算处理卡设计原理图:619-基于6U VPX的双FMC ZU19EG 采集存储计算处理卡
  6. c++练习267题 火柴棒等式
  7. 【面试题】ES6语法五之箭头函数
  8. psgl
  9. 091_解析Callout XML 处理方式
  10. Elasticsearch使用示例