OpenCascade中读取STL文件的相同节点合并器(源码分析)

OpenCascade中读取STL文件的相同节点合并器(源码分析)

一、概述

STL文件的格式是没有节点索引的,是以数值来表示节点的,这就导致如果读入的时候不做去重的话,读取后节点会出现重复的情况,不仅浪费内存,而且可能在运行某些算法的时候出现惊人的错误。

STL的面片格式如下

facet normal  5.223066e-02 -1.736872e-01  9.834148e-01
   outer loop
     vertex  3.002048e+02  9.027633e+01  1.639474e+02
     vertex  2.440949e+02  8.656459e+01  1.662719e+02
     vertex  2.513544e+02  6.242381e+01  1.616227e+02
   endloop
 endfacet

二、代码分析

OpenCascade中的RWStl_Reader.cxx实现了对STL文件的读取,并且做到了去掉重复的点,让我们看一下源码
Class MergeNodeTool用于合并相同的点
首先分析一下MergeNodeTool的父类Poly_MergeNodesTool

构造函数:
public:
//! @param[in] theSmoothAngle smooth angle in radians or 0.0 to disable merging by angle
  //! @param[in] theMergeTolerance node merging maximum distance
  //! @param[in] theNbFacets estimated number of facets for map preallocation
  Standard_EXPORT Poly_MergeNodesTool (const double theSmoothAngle,
                                       const double theMergeTolerance = 0.0,
                                       const int    theNbFacets = -1);
类成员定义:
private:

  Handle(Poly_Triangulation) myPolyData;           //!< output triangulation
  MergedNodesMap             myNodeIndexMap;       //!< map of merged nodes
  NCollection_Map<NCollection_Vec4<int>, MergedElemHasher>
                             myElemMap;            //!< map of elements
  NCollection_Vec4<int>      myNodeInds;           //!< current element indexes
  NCollection_Vec3<float>    myTriNormal;          //!< current triangle normal
  gp_XYZ                     myPlaces[4];          //!< current triangle/quad coordinates to push

  Standard_Real              myUnitFactor;         //!< scale factor to apply
  Standard_Integer           myNbNodes;            //!< number of output nodes
  Standard_Integer           myNbElems;            //!< number of output elements
  Standard_Integer           myNbDegenElems;       //!< number of degenerated elements
  Standard_Integer           myNbMergedElems;      //!< number of merged elements
  Standard_Boolean           myToDropDegenerative; //!< flag to filter our degenerate elements
  Standard_Boolean           myToMergeElems;       //!< flag to merge elements

1、如何判断为相同的节点

在vec3AreEqual中使用两种评判标准,如果myInvTol <= 0 则不启用可容忍的误差来判断两点是否相等,仅当点完全一致时为相同顶点,另一种就是加入了可以容忍的误差,当点足够近的时候,也判断为相同的节点(注:代码中的myInvTol = 1.0/可容忍的误差值,避免因为太小而被判断为 == 0.0 导致没有加入可容忍的误差值)

// =======================================================================
inline bool Poly_MergeNodesTool::MergedNodesMap::vec3AreEqual (const NCollection_Vec3<float>& theKey1,
                                                               const NCollection_Vec3<float>& theKey2) const
{
  if (myInvTol <= 0.0f)
  {
   //! 如果myInvTol <= 0 则不启用可容忍的误差来判断两点是否相等,仅当点完全一致时为相同.
   //bool IsEqual (const NCollection_Vec3& theOther) const
   //{
   //return v[0] == theOther.v[0]
        //&& v[1] == theOther.v[1]
        //&& v[2] == theOther.v[2];
   //}
    //调用了NCollection中的IsEqual方法 对比每一个值来判断是否相等
    return theKey1.IsEqual (theKey2);
   }
  /// tolerance should be smaller than triangle size to avoid artifacts
  //const CellVec3i anIndex1 = vec3ToCell (theKey1);
  //const CellVec3i anIndex2 = vec3ToCell (theKey2);
  //return anIndex1.IsEqual (anIndex2);
  float aVal = theKey1.x() - theKey2.x();
  if (aVal < 0) { aVal = -aVal; }
  if (aVal > myTolerance) { return false; }
  aVal = theKey1.y() - theKey2.y();
  if (aVal < 0) { aVal = -aVal; }
  if (aVal > myTolerance) { return false; }
  aVal = theKey1.z() - theKey2.z();
  if (aVal < 0) { aVal = -aVal; }
  if (aVal > myTolerance) { return false; }
  return true;
}

isEqual方法先是调用了之前点是否相等,其次是对两个点的法向量进行了点乘,由于都是单位向量,点乘的结果等于两个法向量夹角的余弦值,如果结果大于我们设定的cos值时,则说明时同一个顶点,应该合并。
当aCosinus <= -myAngleCos时,如果设定了myToMergeOpposite = true(允许合并具有相反方向法线的节点) 则合并。
(注:这玩意在等会的插入三角形的时候会用到,如果节点判断已经出现过了,则会直接返回id,而不会新增一个节点的id)

// =======================================================================
// function : MergedNodesMap::isEqual
// purpose  :
// =======================================================================
inline bool Poly_MergeNodesTool::MergedNodesMap::isEqual (const Vec3AndNormal& theKey1,
                                                          const NCollection_Vec3<float>& thePos2,
                                                          const NCollection_Vec3<float>& theNorm2,
                                                          bool& theIsOpposite) const
{
  if (!vec3AreEqual (theKey1.Pos, thePos2))
  {
    return false;
  }

  const float aCosinus = theKey1.Norm.Dot (theNorm2);
  if (aCosinus >= myAngleCos)
  {
    //theIsOpposite = false;
    return true;
  }
  else if (myToMergeOpposite
        && aCosinus <= -myAngleCos)
  {
    theIsOpposite = true;
    return true;
  }
  return false;
}

2、Poly_MergeNodesTool中的MergeNodes方法

Handle(Poly_Triangulation) Poly_MergeNodesTool::MergeNodes (const Handle(Poly_Triangulation)& theTris,
                                                            const gp_Trsf& theTrsf,
                                                            const Standard_Boolean theToReverse,
                                                            const double theSmoothAngle,
                                                            const double theMergeTolerance,
                                                            const bool   theToForce)
{
  //判断Poly_Triangulation是否合法
  if (theTris.IsNull()
   || theTris->NbNodes() < 3
   || theTris->NbTriangles() < 1)
  {
    //不合法,直接返回一个空的handle
    return Handle(Poly_Triangulation)();
  }
  //初始化一个合并工具
  Poly_MergeNodesTool aMergeTool (theSmoothAngle, theMergeTolerance, theTris->NbTriangles());
  aMergeTool.AddTriangulation (theTris, theTrsf, theToReverse);
  //theToForce为true的时候代表强制放回新的Poly_Triangulation,即便点的数量和面的数量没有改变 (没有发生合并)
  if (!theToForce
    && aMergeTool.NbNodes()    == theTris->NbNodes()
    && aMergeTool.NbElements() == theTris->NbTriangles())
  {
    return Handle(Poly_Triangulation)();
  }
  //返回结果
  return aMergeTool.Result();
}

aMergeTool.AddTriangulation的代码如下

void Poly_MergeNodesTool::AddTriangulation (const Handle(Poly_Triangulation)& theTris,
                                            const gp_Trsf& theTrsf,
                                            const Standard_Boolean theToReverse)
{
  if (theTris.IsNull())
  {
    return;
  }

  if (!myPolyData.IsNull()
    && myPolyData->NbNodes() == 0)
  {
    // 提前分配好空间
    //设置精度
    myPolyData->SetDoublePrecision (theTris->IsDoublePrecision());
    //设置点的数量
    myPolyData->ResizeNodes        (theTris->NbNodes(), false);
    //设置三角面的数量
    myPolyData->ResizeTriangles    (theTris->NbTriangles(), false);
  }
  //开始遍历所有的三角面
  for (int anElemIter = 1; anElemIter <= theTris->NbTriangles(); ++anElemIter)
  {
    //Poly_Triangle中含有一个int mynodes[3]用来存放三个点的下标
    //获得当前遍历的三角形
    Poly_Triangle anElem = theTris->Triangle (anElemIter);
    //如果需要翻转三角形,则翻转三角形
    if (theToReverse)
    {
      anElem = Poly_Triangle (anElem.Value (1), anElem.Value (3), anElem.Value (2));
    }
    //遍历这个面的每个顶点,并进行空间的变换到世界坐标下
    for (int aTriNodeIter = 0; aTriNodeIter < 3; ++aTriNodeIter)
    {
      const gp_Pnt aNode = theTris->Node (anElem.Value (aTriNodeIter + 1)).Transformed (theTrsf);
      //myPlaces是类成员,可以用来存放当前三角面的三个顶点或者四边形网格的四个顶点
      myPlaces[aTriNodeIter] = aNode.XYZ();
    }
    //开始把三角形数据插入到我们类成员的myPolyData
    //这一步非常的关键
    PushLastTriangle();
  }
}

PushLastTriangle();这一步非常的重要,是合并代码的核心!具体流程如下

  1. 调用PushLastElement (3);
  2. 判断点的数量是否合法
  3. 判断是否设置了可容忍的误差(Angle和Tolerance)
    3.1. 如果开启了,判断是否可以跳过角度检查(angle接近90度 ),如果不能跳过,则计算三角形的法向量并保存。调用pushNodeCheck方法对每一个点进行检查,查找在哈希表中是否存在相同或者相近的顶点,如果存在则直接放回index值,如果不存在则需要新增顶点,保存在哈希表和myPolyData中。
    3.2. 如果没有开启,则直接调用pushNodeNoMerge对每个点进行插入,不进行去重操作。
  4. 其他一些选项的检查
  5. 在myPolyData中新增三角面

代码如下:

void Poly_MergeNodesTool::PushLastElement (int theNbNodes)
{
  if (theNbNodes != 3
   && theNbNodes != 4)
  {
    throw Standard_ProgramError ("Poly_MergeNodesTool::PushLastElement() - Internal error");
  }

  bool isOpposite = false;
  myNodeInds[3] = -1;
  //判断是否具有合并操作
  if (myNodeIndexMap.HasMergeAngle()
   || myNodeIndexMap.HasMergeTolerance())
  {
    if (!myNodeIndexMap.ToMergeAnyAngle())
    {
      myTriNormal = computeTriNormal();
    }
    //合并节点操作
    pushNodeCheck (isOpposite, 0);
    pushNodeCheck (isOpposite, 1);
    pushNodeCheck (isOpposite, 2);
    if (theNbNodes == 4)
    {
      pushNodeCheck (isOpposite, 3);
    }
  }
  else
  {
    //直接插入操作
    pushNodeNoMerge (0);
    pushNodeNoMerge (1);
    pushNodeNoMerge (2);
    if (theNbNodes == 4)
    {
      pushNodeNoMerge (3);
    }
  }
  //判断是否有面退化成边了
  if (myToDropDegenerative)
  {
    // warning - removing degenerate elements may produce unused nodes
    if (myNodeInds[0] == myNodeInds[1]
     || myNodeInds[0] == myNodeInds[2]
     || myNodeInds[1] == myNodeInds[2])
    {
      if (theNbNodes == 4)
      {
        //
      }
      else
      {
        ++myNbDegenElems;
        return;
      }
    }
  }
  //如果开启了合并三角面
  if (myToMergeElems)
  {
    NCollection_Vec4<int> aSorted = myNodeInds;
    std::sort (aSorted.ChangeData(), aSorted.ChangeData() + theNbNodes);
    if (!myElemMap.Add (aSorted))
    {
      ++myNbMergedElems;
      return;
    }
  }
  //在myPolyData中新增新的三角形
  ++myNbElems;
  if (!myPolyData.IsNull())
  {
    if (myPolyData->NbTriangles() < myNbElems)
    {
      myPolyData->ResizeTriangles (myNbElems * 2, true);
    }
    myPolyData->SetTriangle (myNbElems, Poly_Triangle (myNodeInds[0] + 1, myNodeInds[1] + 1, myNodeInds[2] + 1));
    if (theNbNodes == 4)
    {
      ++myNbElems;
      if (myPolyData->NbTriangles() < myNbElems)
      {
        myPolyData->ResizeTriangles (myNbElems * 2, true);
      }
      myPolyData->SetTriangle (myNbElems, Poly_Triangle (myNodeInds[0] + 1, myNodeInds[2] + 1, myNodeInds[3] + 1));
    }
  }
}

检查是否需要合并顶点操作并新增顶点

void pushNodeCheck (bool& theIsOpposite,
                      const int theTriNode)
  {
    int aNodeIndex = myNbNodes;
    const gp_XYZ& aPlace = myPlaces[theTriNode];
    const NCollection_Vec3<float> aVec3 ((float )aPlace.X(), (float )aPlace.Y(), (float )aPlace.Z());
    if (myNodeIndexMap.Bind (aNodeIndex, theIsOpposite, aVec3, myTriNormal))
    {
      ++myNbNodes;
      if (!myPolyData.IsNull())
      {
        if (myPolyData->NbNodes() < myNbNodes)
        {
          myPolyData->ResizeNodes (myNbNodes * 2, true);
        }
        myPolyData->SetNode (myNbNodes, aPlace * myUnitFactor);
      }
    }
    myNodeInds[theTriNode] = aNodeIndex;
  }

// =======================================================================
// function : MergedNodesMap::Bind
// purpose  :
// =======================================================================
inline bool Poly_MergeNodesTool::MergedNodesMap::Bind (int& theIndex,
                                                       bool& theIsOpposite,
                                                       const NCollection_Vec3<float>& thePos,
                                                       const NCollection_Vec3<float>& theNorm)
{
  if (Resizable())
  {
    ReSize (Extent());
  }

  //NCollection_ListNode** NCollection_BaseMap::myData1
  //是基础map中的一个二级指针
  DataMapNode** aData = (DataMapNode** )myData1;
  const int aHash = hashCode (thePos, theNorm, NbBuckets());
  for (DataMapNode* aNodeIter = aData[aHash]; aNodeIter != NULL;
       aNodeIter = (DataMapNode* )aNodeIter->Next())
  {
    //比较两个点是否相同
    if (isEqual (aNodeIter->Key(), thePos, theNorm, theIsOpposite))
    {
      //访问可变的value值
      //TheItemType& ChangeValue () { return myValue; }
      theIndex = aNodeIter->ChangeValue();
      //已经存在节点,无需绑定
      return false;
    }
  }
  //没有找到完全相同的点,查看是否允许误差的存在,如果存在利用误差进行判断
  if (myInvTol > 0.0f)
  {
    //非常妙的判断,利用可容忍度的倒数,将点的坐标转化成int,并且在xyz方向上增减1来判断是否相近
    //myInvTol = 1.0/可容忍度
    //例:可容忍度为0.001 则myInvTol=1000,点的坐标全部乘1000
    //   当点的坐标为0.45333时,会被放大到(int)453,此时454或453或452都会被认为是相同
    //   的点,下面的THE_NEIGHBRS就是用来帮助判断的。
    static const CellVec3i THE_NEIGHBRS[26] =
    {
      CellVec3i(-1, 0, 0),CellVec3i( 1, 0, 0),CellVec3i( 0,-1, 0),CellVec3i( 0, 1, 0),CellVec3i( 0, 0,-1),CellVec3i( 0, 0, 1),
      CellVec3i(-1,-1, 0),CellVec3i( 1,-1, 0),CellVec3i( 1, 1, 0),CellVec3i(-1, 1, 0),
      CellVec3i( 0,-1,-1),CellVec3i( 0, 1,-1),CellVec3i( 0, 1, 1),CellVec3i( 0,-1, 1),
      CellVec3i(-1, 0,-1),CellVec3i( 1, 0,-1),CellVec3i( 1, 0, 1),CellVec3i(-1, 0, 1),
      CellVec3i(-1,-1,-1),CellVec3i( 1,-1,-1),CellVec3i(-1, 1,-1),CellVec3i( 1, 1,-1),CellVec3i(-1,-1, 1),CellVec3i( 1,-1, 1),CellVec3i(-1, 1, 1),CellVec3i(1, 1, 1)
    };
    const CellVec3i anIndexCnt = vec3ToCell (thePos);
    for (int aNeigIter = 0; aNeigIter < 26; ++aNeigIter)
    {
      const CellVec3i anIndex = anIndexCnt + THE_NEIGHBRS[aNeigIter];
      const int aHashEx = vec3iHashCode (anIndex, NbBuckets());
      for (DataMapNode* aNodeIter = aData[aHashEx]; aNodeIter != NULL;
           aNodeIter = (DataMapNode* )aNodeIter->Next())
      {
        if (isEqual (aNodeIter->Key(), thePos, theNorm, theIsOpposite))
        {
          theIndex = aNodeIter->ChangeValue();
          return false;
        }
      }
    }
  }
  //theIsOpposite = false;
  //如果上述的情况都不存在,则新增一个节点
  aData[aHash] = new (this->myAllocator) DataMapNode (thePos, theNorm, theIndex, aData[aHash]);
  //map中的mysize++
  Increment();
  return true;
}

至此,Poly_MergeNodesTool的大致运行机制都已经了解了,就是利用map对点来进行合并,对每个面都进行遍历,将认为是相同的点给设置相同的index,这样就保证了不会出现重合的顶点,接下来就是对Poly_MergeNodesTool的继承实现。

3、MergeNodeTool(继承于Poly_MergeNodesTool)

MergeNodeTool中重写了AddTriangle方法,主要是在原来的方法上新增了一些对于点索引的维护,同时新增了两个成员变量

  private:
    //一个读的指针
    RWStl_Reader* myReader;
    //维护顶点索引的map
    NCollection_DataMap<Standard_Integer, Standard_Integer> myNodeIndexMap;
class MergeNodeTool : public Poly_MergeNodesTool
  {
  public:

    //! Constructor
    MergeNodeTool (RWStl_Reader* theReader,
                   const Standard_Integer theNbFacets = -1)
    : Poly_MergeNodesTool (theReader->MergeAngle(), 0.0, theNbFacets),
      myReader (theReader),
      myNodeIndexMap (1024, new NCollection_IncAllocator (1024 * 1024))
    {
      // avoid redundant allocations as final triangulation is managed by RWStl_Reader subclass
      ChangeOutput().Nullify();
    }

    //! Add new triangle
    void AddTriangle (const gp_XYZ theElemNodes[3])
    {
      //调用父类的增加三角形方法
      Poly_MergeNodesTool::AddTriangle (theElemNodes);

      // remap node indices returned by RWStl_Reader::AddNode();
      // 对于顺序增加是一个很耗费时间的方法, 但是保留了,因为这是STLAPI的接口
      //ElementNodeIndex返回的是当前处理的三角形的顶点下标
      int aNodesSrc[3] = { ElementNodeIndex (0), ElementNodeIndex (1), ElementNodeIndex (2) };
      int aNodesRes[3] = { -1, -1, -1 };
      for (int aNodeIter = 0; aNodeIter < 3; ++aNodeIter)
      {
        // 查找map中是否有对应下标已经存储过了,如果有则直接填入aNodesRes中
        if (!myNodeIndexMap.Find (aNodesSrc[aNodeIter], aNodesRes[aNodeIter]))
        {
          //此时没有相同的点,则增加新的节点到当前模型并返回下标
          aNodesRes[aNodeIter] = myReader->AddNode (theElemNodes[aNodeIter]);
          //绑定原来的index和当前模型下的index
          myNodeIndexMap.Bind (aNodesSrc[aNodeIter], aNodesRes[aNodeIter]);
        }
      }
      //增加面
      if (aNodesRes[0] != aNodesRes[1]
       && aNodesRes[1] != aNodesRes[2]
       && aNodesRes[2] != aNodesRes[0])
      {
        myReader->AddTriangle (aNodesRes[0], aNodesRes[1], aNodesRes[2]);
      }
    }

  private:
    RWStl_Reader* myReader;
    NCollection_DataMap<Standard_Integer, Standard_Integer> myNodeIndexMap;
  };

三、总结

整体来说这一块的设计还是非常妙的,但是代码有点难读,如果不理解的话可以自己再看看源码,基本上用到下面的文件

  • RWStl_Reader.hxx | RWStl_Reader.cxx
  • Poly_MergeNodesTool.hxx | Poly_MergeNodesTool.hxx
  • NCollection_DataMap.hxx | NCollection_DataMap.cxx
  • Poly_Triangulation.hxx | Poly_Triangulation.cxx
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值