如何在自己的项目中使用BVH(层次包围盒)
使用BVH的场景
BVH可用在碰撞检测、光线追踪等场景,但由于项目的局限性,本文仅针对碰撞检测做出一定的介绍。
怎么确定BVH的使用方向
众所周知,在Opencascade中,模型是由TopoDS和AIS_Shape共同组成的,TopoDS控制模型的拓扑结构,AIS_Shape控制模型的显示样式。基于这个前提下,我先给出结论:需要用布尔运算计算出的新模型,就用TopoDS相关的布尔运算。如果是想做碰撞检测、光追,就用AIS_Shape中的Mesh(三角网格)计算
在碰撞检测中,操作TopoDS和AIS_Shape的区别
TopoDS是模型的根本,所以官方提供了一般的TopoDS算法,比如BRepExtrema_DistShapeShape
和BRepAlgoAPI_Common
等方法。这些方法虽然可以计算出模型的距离或者新模型,但计算的时间太长了,根本无法满足项目的要求。
简单来说,如果要计算两个模型的距离,就算BRepExtrema_DistShapeShape
只需要几秒钟的计算,但在实时碰撞检测的情况下,这个时间是远远无法满足软件要求的。这也是为什么需要引入AIS_Shape(Mesh计算)
模型 | 在碰撞检测中的计算时间 |
---|---|
TopoDS_Shape | 方法多样,但很多计算多余,且耗时 |
AIS_Shape(Mesh) | 计算超级快,我的项目里至少提速50倍,但用起来不方便,没有现成的BVH方法直接操作自己的模型 |
BVH操作AIS_Shape(三角网格)的具体方法
我这里一直说AIS_Shape,只是想泛指渲染的数据,我想大部分新手一开始接触Opencascade都对AIS_Shape有所了解,而不是其他专业的概念
构建好BVH容器
// Define BVH Builder
opencascade::handle <BVH_LinearBuilder <Standard_Real, 3> > aLBuilder = new BVH_LinearBuilder <Standard_Real, 3>();
// Create the ShapeSet
opencascade::handle <BVH_BoxSet <Standard_Real, 3, std::vector<BVH_Vec3d>> > aTriangleBoxSet[2]; // 我需要计算两个Shape的距离,所以是声明两个BoxSet
上述代码中,容器是通过智能指针构建的,这也是很重要的一点。虽然也可以直接声明一个局部变量,但后续移动遍历、拷贝变量的时候会特别耗时,会导致算法变得很慢。
提取TopoDS每个面的网格信息
上文说到BVH操作Mesh不太方便,也是因为这个问题。我们需要手动的提取出TopoDS的网格信息,比如:
TopoDS_Shape A = BRepBuilderAPI_Transform(tool.toolHeads[toolNum]->Shape(), tool.toolHeads[toolNum]->Transformation()).Shape();
TopTools_IndexedMapOfShape aMapShapes;
TopExp::MapShapes(A, TopAbs_FACE, aMapShapes);
for (Standard_Integer iS = 1; iS <= aMapShapes.Extent(); ++iS)
{
TopoDS_Face face = TopoDS::Face(aMapShapes(iS));
TopLoc_Location aLoc;
Handle(Poly_Triangulation) aTriangulation = BRep_Tool::Triangulation(face, aLoc);
const int aNbTriangles = aTriangulation->NbTriangles();
//...
}
上述代码中,有2个要注意的点:
TopoDS
要经过BRepBuilderAPI_Transform
移动,移动的依据是渲染出来的AIS_Shape,因为Opencascade中,AIS_Shape的移动不会带着TopoDS跑,如果想要准确的三角网格信息,需要把TopoDS和AIS_Shape的位置关联起来,再用BRep_Tool::Triangulation
- 需要遍历每个面,将每个面进行三角网格划分。
将网格数据存入BVH_BoxSet
我们需要先声明几个变量,这几个变量的作用是初始化BVH_Box。
// Define BVH Builder
opencascade::handle <BVH_LinearBuilder <Standard_Real, 3> > aLBuilder = new BVH_LinearBuilder <Standard_Real, 3>();
// Create the ShapeSet
opencascade::handle <BVH_BoxSet <Standard_Real, 3, std::vector<BVH_Vec3d>> > aTriangleBoxSet[2];
// 构建shape1的BVH
aTriangleBoxSet[0] = new BVH_BoxSet <Standard_Real, 3, std::vector<BVH_Vec3d>>(aLBuilder);
上述代码中,我们声明了aTriangleBoxSet[0],即有了一个空的BVH_Box,在它的基础上,我们不断的往盒子中添加三角网格划分出来的三角顶点,就可以生成一个BVH_Box了
for (int iT = 1; iT <= aNbTriangles; ++iT)
{
const Poly_Triangle aTriangle = aTriangulation->Triangle(iT);
// Nodes indices
Standard_Integer id1, id2, id3;
aTriangle.Get(id1, id2, id3);
const gp_Pnt aP1 = aTriangulation->Node(id1).Transformed(aLoc.Transformation()).Transformed(shape1->Transformation());
const gp_Pnt aP2 = aTriangulation->Node(id2).Transformed(aLoc.Transformation()).Transformed(shape1->Transformation());
const gp_Pnt aP3 = aTriangulation->Node(id3).Transformed(aLoc.Transformation()).Transformed(shape1->Transformation());
BVH_Vec3d aBVHP1(aP1.X(), aP1.Y(), aP1.Z());
BVH_Vec3d aBVHP2(aP2.X(), aP2.Y(), aP2.Z());
BVH_Vec3d aBVHP3(aP3.X(), aP3.Y(), aP3.Z());
BVH_Box<Standard_Real, 3> aBox;
aBox.Add(aBVHP1);
aBox.Add(aBVHP2);
aBox.Add(aBVHP3);
std::vector<BVH_Vec3d> vecs{ aBVHP1, aBVHP2, aBVHP3 };
aTriangleBoxSet[0]->Add(vecs, aBox);
}
自定义BVH_Box的计算方法
直接上代码:
class MeshCollideDetection : public BVH_PairDistance<Standard_Real, 3, BVH_BoxSet<Standard_Real, 3, std::vector<BVH_Vec3d>>>
{
// Structure to contain points of the triangle,此结构体来自源码,其实std::vector构建三个节点的结构也行
// 这个结构体写在此处是不够的,还得找别的地方存。比较麻烦,我就直接用std::vector存了 --wuxin
//public:
// struct Triangle
// {
// Triangle() {}
// Triangle(const BVH_Vec3d& theNode1,
// const BVH_Vec3d& theNode2,
// const BVH_Vec3d& theNode3)
// : Node1 (theNode1), Node2 (theNode2), Node3 (theNode3)
// {}
//
// BVH_Vec3d Node1;
// BVH_Vec3d Node2;
// BVH_Vec3d Node3;
// };
public:
MeshCollideDetection(const Handle(AIS_Shape)& tool_Ais, opencascade::handle < BVH_BoxSet <Standard_Real, 3, std::vector<BVH_Vec3d>>> wheelMeshData)
: _Tool_Ais(tool_Ais), _wheelMeshBox(wheelMeshData)
{}
public:
//! Defines the rules for leaf acceptance
virtual Standard_Boolean Accept(const Standard_Integer theIndex1,
const Standard_Integer theIndex2) Standard_OVERRIDE
{
const std::vector<BVH_Vec3d>& aTri1 = myBVHSet1->Element(theIndex1);
const std::vector<BVH_Vec3d>& aTri2 = myBVHSet2->Element(theIndex2);
Standard_Real aDistance = CalculateTriangleDistance(aTri1, aTri2);
if (aDistance < myDistance)
{
myDistance = aDistance;
return Standard_True;
}
return Standard_False;
}
double BVH_Vec_Distance(const BVH_Vec3d& p1, const BVH_Vec3d& p2);
double Point2SegmentDistance(const std::vector<BVH_Vec3d>& vecs);
double TriangleToPointDistance(const std::vector<BVH_Vec3d>& tri, const BVH_Vec3d& point);
double CalculateTriangleDistance(const std::vector<BVH_Vec3d>& tri1, const std::vector<BVH_Vec3d>& tri2);
double CalMeshDis_From2AIS_Shape(const Handle(AIS_Shape)& shape1, const Handle(AIS_Shape)& shape2);
double CalMeshDis_From2Shape();
private:
Handle(AIS_Shape) _Tool_Ais;
opencascade::handle < BVH_BoxSet <Standard_Real, 3, std::vector<BVH_Vec3d>>> _wheelMeshBox;
上述代码的核心部分是Accept
和CalculateTriangleDistance
。其中Accept
是自定义筛选BVH节点的方法,在计算两个Mesh的距离时,此方法可以不修改。CalculateTriangleDistance
是计算两个三角形之间距离的方法,这个方法是需要自己写的,在官方提供的源码中,有一个BVH_Tools::PointTriangleSquareDistance
可以计算一个点(也就是BVH_Vec3d
)和其他三个BVH_Vec3d
的距离的方法,可以直接拿来用,我们只需要分别把三角形中的几个点和另外一个三角形中的三个点做个距离计算,取这几组计算的最小值就可以了。
我的demo是:
double MeshCollideDetection::TriangleToPointDistance(const std::vector<BVH_Vec3d>& tri, const BVH_Vec3d& point)
{
// 自己实现的方法
/*double d1 = Point2SegmentDistance({ point, tri[0], tri[1] });
double d2 = Point2SegmentDistance({ point, tri[1], tri[2] });
double d3 = Point2SegmentDistance({ point, tri[2], tri[1] });
return Math::Min(d3, Math::Min(d1, d2));*/
// 库里的方法,它的结果是距离的平方
return BVH_Tools<Standard_Real, 3>::PointTriangleSquareDistance(point, tri[0], tri[1], tri[2]);
}
double MeshCollideDetection::CalculateTriangleDistance(const std::vector<BVH_Vec3d>& tri1, const std::vector<BVH_Vec3d>& tri2)
{
double d1 = TriangleToPointDistance(tri1, tri2[0]);
double d2 = TriangleToPointDistance(tri1, tri2[1]);
double d3 = TriangleToPointDistance(tri1, tri2[2]);
double d4 = TriangleToPointDistance(tri2, tri1[0]);
double d5 = TriangleToPointDistance(tri2, tri1[1]);
double d6 = TriangleToPointDistance(tri2, tri1[2]);
std::vector<double> ds{ d1,d2,d3,d4,d5,d6 };
double min = 1e5;
for (double d : ds)
{
if (d < min) min = d;
}
return min;
}
终于到最后一步,计算距离
有了俩包含Mesh信息的BVH_Box后,使用BVH_PairDistance
这个类下的SetBVHSets
,再调用它的ComputeDistance
获取距离即可:
SetBVHSets(aTriangleBoxSet[0].get(), aTriangleBoxSet[1].get());
Standard_Real dis = ComputeDistance();
计算结果
注意事项
- 我参与的项目中,一个MeshA有9w+的三角面,另外一个MeshB有12个三角面。即便BVH的计算很快,但MeshA的BVH_Box生成速度并不算太快(相比计算而言)。但是,结合实际情况,我的MeshA可以提前计算,当MeshB不断移动的时候,我可以反复利用MeshA的BVH_Box,避免层次包围盒的计算,极大的提升实时移动下的检测效率。
- BVH_Box最好用智能指针包裹,否则C++的值传递会让你怀疑人生。
- 由于项目中的代码是很庞大的,我在这篇文章中贴的demo是没办法直接使用的,只是一些核心的代码,还请读者有选择的进行修改。
后续想法
在此基础上,其他模型的Mesh检测也是有必要的,不能针对某两个模型进行检测。但移动的模型变多了,实时计算层次包围盒的模型也会变多,导致检测效率直线下降。我的想法是,有计划的减少其他模型的Mesh精度,生成较少的三角面,这样处理起来会快些。但这个做法我还在研究中,如果有读者知道如何减少特定模型的Mesh精度,可以教我一下。
参考链接
eryar大佬写的BVH文章: 性能提升-BVH层次包围体
.