Loading
0

3D格式转换神器HOOPS Exchange使用教程(二):检索可视化工作流的图形数据

HOOPS Exchange是什么?

HOOPS Exchange 是一组软件库,可以帮助开发人员在开发应用程序时读取和写入主流的 2D 和 3D 格式。HOOPS Exchange 支持在主流的3D 文件格式中读取 CAD 数据,并支持将 3D 数据转换为 PRC 数据格式,这是一种高度可压缩和开放的文件格式,并已通过国际标准化组织 (ISO 14739-1:2014) 的认证。PRC 也是 Adobe PDF 中用于 3D 的格式之一。HOOPS Exchange 持续优化读取各种 3D 数据的功能,尤其是对于来自计算机辅助设计 (CAD) 系统的数据。

本章我们学习创建一个使用 HOOPS Exchange 加载文件并使用 Qt3D 将其可视化的跨平台应用程序。

介绍
本教程将向大家说明如何使用 HOOPS Exchange 检索可视化工作流的图形数据。学习完本教程后,您将对 HOOPS Exchange 如何提供对零件三角形网格的访问、如何在 3D 空间中正确定位它们以及如何确定每个零件的基本颜色有一个基本的了解。
本教程有一些先决条件。首先,您应该已经完成了“打印装配结构”教程,该教程涵盖了文件加载和数据检索等几个基本概念,这些话题在此不再赘述。
HOOPS Exchange 是一个支持 Windows、macOS 和 Linux 的 SDK。我们将使用最流行的跨平台 GUI 工具包 Qt,具体来说,我们将依赖 Qt3D 来实现跨平台的图形功能。我们将尽一切努力将工具包所需的专业知识降至最低,但是,您必须在计算机上安装 Qt 6才能完成本教程。
像许多跨平台开发社区一样,Qt 已经开始向使用 CMake 作为默认构建系统的方向迁移。可以在此处找到有关使用 CMake 构建 Qt 应用程序的信息。本教程包括基于这些概念的完整 CMakeLists.txt 文件。Qt 的最新发行版包括 bin/qt-cmake,如果您尚未安装 CMake,则可以使用它们。
不需要深入了解 Qt 和 CMake,但两者都必须安装并准备好使用。

第 0 步:项目设置
克隆项目
我们提供了一个 git 存储库来支持本教程。克隆主分支以建立项目的起点。
git 克隆 https://github.com/techsoft3d/he_qt_basic_view.git
配置
使用您喜欢的文本编辑器打开文件CMakeLists.txt。在文件的顶部,您将看到HOOPS_EXCHANGE_DIR已设置变量。更新分配给此变量的值以反映您的特定安装位置。
建造
由于本教程的目标是提供对 HOOPS Exchange 的理解,因此我们不会花太多时间在如何构建和运行 Qt 应用程序或 IDE 选择和配置的主题上。但以防万一您不熟悉它是如何完成的,我们将在此处提供一些提示。
视觉工作室代码
Visual Studio Code 是跨平台开发的绝佳选择。它支持 C/C++ 开发和 CMake 作为构建配置系统。Microsoft在此处提供了此用例的出色概述。
编辑文件 _.vscode/settings.json_ 并更新 Qt 路径以反映您本地安装的 Qt。安装 CMake Tools 扩展后,您可以使用状态栏上的按钮来配置、构建和运行应用程序。
Windows 上的 Visual C++
打开 Visual Studio 命令提示符并执行位于 Qt 安装的 bin 文件夹中的 qtenv2.bat。接下来,在项目目录中创建一个名为build的子文件夹并更改为它。运行qt-cmake ..以生成所需的文件。这将创建qt_he_viewer.sln,您可以使用命令evenv qt_he_viewer.sln 打开它。
开始运作
构建项目后,您就可以运行应用程序了。当您运行二进制文件时,您将看到一个标准的文件打开对话框。对话框的默认位置是包含 HOOPS Exchange 附带的示例数据的文件夹。导航到 PRC 子文件夹并选择helloworld.prc。该文件加载迅速,并出现空的 3D 视图。
查看main.cpp的实现以熟悉程序流程。您会注意到 HOOPS Exchange 已初始化,并提示用户输入一个输入文件,然后加载该文件。加载文件后,代码继续调用createScene,配置视图、相机和光源。
我们将从创建场景开始,以一种有点抽象的方式。
第 1 步:创建场景
要创建场景,我们必须实现Scene.cppcreateScene中定义的函数。在编辑器中打开文件。你会注意到它被存根返回一个空对象。
在 HOOPS Exchange 数据模型中,曲面细分存在于表示项级别。这意味着我们将需要实现遍历装配结构、输入每个零件定义并提取其中包含的表示项的功能。对于我们遇到的每个表示项目,我们需要做一些事情:

  1. 确定是否应显示表示项。
  2. 生成我们可以轻松渲染的细分数据。
  3. Qt3D从HOOPS Exchange 细分创建网格。
  4. Qt3D从HOOPS Exchange 样式定义创建材质。
  5. Qt3D从世界位置创建一个变换。

我们刚刚列出的所有功能都已在您克隆的项目中被删除,因此我们可以编写完整的 createScene 主体,而无需过多关注每个步骤的实现方式。
首先,我们将声明并初始化一个结构来控制如何为表示项生成镶嵌。创建后添加以下代码行rootEntity.
// 创建曲面细分参数来控制行为
A3DRWParamsTessellationData tess_params;
A3D_INITIALIZE_DATA(A3DRWParamsTessellationData, tess_params);
// 使用“预设”选项获得中等详细程度
tess_params.m_eTessellationLevelOfDetail = kA3DTessLODMedium;
为简单起见,我们在 options 结构中使用详细级别枚举,它控制一组特定的细分选项。这适用于基本的查看工作流程。我们将很快使用这个选项对象。
forEach_RepresentationItem接下来,我们将使用稍后实现的函数来迭代每个表示项。现在,让我们假设它存在并且做我们想做的事——也就是说,它遍历装配结构,并且对于它遇到的每个零件,它都提取表示项。对于每个表示项,调用提供的 lambda。设置细分参数后添加以下代码行。
// 遍历每个表示项
forEach_RepresentationItem(model_file, [&](EntityArray const &path) {
});
lambda 的参数是 an EntityArray,,它是 的类型别名QVector<A3DEntity*>。它包含指向程序集层次结构中每个节点的有序指针列表。数组中的第一项是模型文件,然后是一系列产品,然后是零件。最后,数组以遇到的表示项结束。
对于这一步的其余部分,我们将按顺序将代码添加到 lambda 的主体中。
有时不应绘制表示项。为了确定这一点,我们将使用一种称为级联属性的机制。级联属性允许我们在实例化它的组件的上下文中计算零件的属性。特定装配可以覆盖特定零件的颜色或可见性。我们将把我们对级联属性的使用封装在一个名为的简单结构CascadedAttributes中,稍后我们将实现该结构。它被淘汰了,所以现在让我们假设它的行为符合我们的需要。
在 lambda 的主体中添加以下代码行:
CascadedAttributes ca( 路径 );
// 确定是否应该跳过此项
如果( ca->m_bRemoved || !ca->m_bShow ) {
  返回;
}
CascadedAttributes重载,提供对其中包含的结构operator->的直接访问。A3DMiscCascadedAttributesData如果表示项目的这个实例被删除或不应该显示,我们会提前退出。
如果我们不及早退出,下一步就是在 Exchange 中生成曲面细分。为此,我们添加以下代码行:
A3DRiRepresentationItem *ri = path.back();
// 使用我们上面声明的选项生成曲面细分
A3DRiRepresentationItemComputeTessellation(ri, &tess_params);
现在我们已经对表示项进行了细分,我们可以访问数据。
// 获取此表示项的数据
A3DRiRepresentationItemData摆脱;
A3D_INITIALIZE_DATA(A3DRiRepresentationItemData,摆脱);
if ( A3D_SUCCESS != A3DRiRepresentationItemGet( ri, &rid ) ) {
  返回;
}
// 曲面细分存储在 m_pTessBase 中
自动tess_base = rid.m_pTessBase;
您应该非常熟悉上面介绍的模式,它使用不透明的对象句柄 ( ri) 将其关联数据读入结构。然后从结构中获得镶嵌句柄,我们就可以使用它了。
使用曲面细分的句柄,我们接下来尝试创建一个Qt3D网格。如果我们成功了,我们就会创造并应用它的材料并进行转换。这是通过以下方式完成的,使用了一些已经被删除的附加函数:
// 创建网格
如果(自动网格= createMesh(tess_base)){
  自动节点 =新Qt3DCore::QEntity(rootEntity);
  节点->添加组件(网格);
  // 创建材质
  如果(自动材料= createMaterial(ca->m_sStyle)){
    节点->添加组件(材料);
  }
  // 创建变换
  如果(自动变换 = createTransform(路径)){
    节点->添加组件(变换);
  }
}
如果获得了网格,我们将创建一个节点来保存它,以及材质和变换。该节点是rootEntity.
仍然在 lambda 的主体内工作,我们还有最后一项任务。回想一下,每当您从 Exchange 读取数据时,您必须确保通过第二次调用 getter 并提供空句柄来释放任何关联的内存。
使用 lambda 主体内的以下(也是最终)代码行释放表示项数据:
A3DRiRepresentationItemGet( nullptr , &rid);
这样就完成了构建场景的高层实现。我们显然为以后的步骤留下了许多实现细节,但我们已经完成了构成渲染模型所需的基本场景图的任务。

第 2 步:程序集遍历
从上一步来看,应该有点清楚还剩下什么要做。我们将以系统的方式攻击每个任务,首先通过实现 ForEach_RepresentationItem 遍历程序集层次结构。
让我们从函数必须如何运行的简短描述开始。在您的编辑器中打开文件 ForEachRepresentationItem.cpp,您将找到代码的存根版本:
命名空间{
  void forEach_Impl( EntityArray const &path, std::function< void (EntityArray
  常量&)>常量&fcn ) {
    Q_UNUSED(路径);
    Q_UNUSED(fcn);
  }
}
无效forEach_RepresentationItem(A3DAsmModelFile *model_file,
std::function< void (EntityArray const &)> const &fcn ) {
  forEach_Impl( { model_file }, fcn );
}
该函数有两个参数。第一个是模型文件的不透明句柄。第二个参数是作为回调调用的函数对象。并且,正如我们在第 1 步中所讨论的,实现预计将遍历装配结构并为遇到的每个表示项调用回调。
回调函数使用单个参数调用:一个EntityArray包含 Exchange 对象的不透明句柄的有序列表。该列表是顺序的,从A3DAsmModelFile句柄开始,然后是一个或多个A3DAsmProductOccurrence句柄。句柄代表通向零件的装配层次。当然,接下来就是A3DAsmPartDefinition手柄了。最后,路径包含A3DRiRepresentationItem遇到的句柄。如果部件定义包含A3DRiSet对象(表示项集),则路径中将有多个A3DRiRepresentationItem句柄。
公共函数立即调用一个匿名实现,该实现采用一个EntityArray而不是一个A3DAsmModelFile句柄。这样做的用处很快就会变得清晰。该实现将只关心提供的路径中的最后一个句柄。
一个很好的起点是一开始。所以,让我们实现我们已经知道的情况——当这个函数被路径中的单个对象调用时,它是一个A3DAsmModelFile句柄。在这种情况下,我们希望将每个子A3DAsmProductOccurrence句柄添加到路径并再次调用该函数以进行更深入的挖掘。它应该看起来像这样:
auto  const ntt = path.back();
自动类型 = kA3DTypeUnknown;
if (A3D_SUCCESS != A3DEntityGetType(ntt, &type) ) {
  返回;
}
EntityArray children;
如果(kA3DTypeAsmModelFile == 类型){
  A3DAsmModelFileData mfd;
  A3D_INITIALIZE_DATA(A3DAsmModelFileData, mfd);
  如果(A3D_SUCCESS!= A3DAsmModelFileGet(ntt,&mfd)){
    返回;
  }
  children = EntityArray(mfd.m_ppPOOccurrences,mfd.m_ppPOOccurrences +
  mfd.m_uiPOOccurrencesSize);
  A3DAsmModelFileGet( nullptr , &mfd);
}
对于(auto child : children ){
  自动child_path = 路径;
  child_path.push_back(children auto child : children);
  forEach_Impl(child_path, fcn);
}
A3DAsmProductOccurrence此实现是递归的,并使用句柄作为 的值调用自身path.back()。让我们通过添加 if 子句来扩充处理这种情况的代码。

否则 if ( kA3DTypeAsmProductOccurrence == type ) {
  A3DAsmProductOccurrenceData 吊舱;
  A3D_INITIALIZE_DATA(A3DAsmProductOccurrenceData, pod);
  if (A3D_SUCCESS != A3DAsmProductOccurrenceGet(ntt, &pod) ) {
    返回;
  }
 child = EntityArray( pod.m_ppPOccurrences, pod.m_ppPOccurrences +
  pod.m_uiPOOccurrencesSize );
  A3DAsmProductOccurrenceGet( nullptr , &pod);
}
从这里去哪里?这将处理整个装配层次结构,直至节点包含零件。所以,除了上面实现中所示的处理children外,我们还必须检查an是否A3DAsmProductOccurrence包含一个part。
确定零件是否存在有时就像检查m_pPart产品出现结构中的字段一样简单。但这并没有捕捉到共享部件实例化的常见情况。零件实例化是通过使用m_pPrototype句柄来实现的,该句柄引用了装配节点的共享定义。如果一个节点有一个空m_pPart句柄,你还必须递归检查它的原型,如果它有一个。要实现此逻辑,请在匿名命名空间的顶部添加 getPart 函数。
A3DAsmPartDefinition *getPart( A3DAsmProductOccurrence *po ) {
  if ( nullptr == po ) {
    返回 空指针;
  }
  A3DAsmProductOccurrenceData 吊舱;
  A3D_INITIALIZE_DATA(A3DAsmProductOccurrenceData, pod);
  if (A3D_SUCCESS != A3DAsmProductOccurrenceGet( po, &pod ) ) {
    返回 空指针;
  }
  汽车零件 = pod.m_pPart ?pod.m_pPart : getPart( pod.m_pPrototype );
  A3DAsmProductOccurrenceGet( nullptr , &pod);
  返回部分;
  }
现在,我们可以在刚刚添加的处理A3DAsmPartDefinition对象的子句中使用这个函数:
否则 if ( kA3DTypeAsmProductOccurrence == type ) {
  A3DAsmProductOccurrenceData 吊舱;
  A3D_INITIALIZE_DATA(A3DAsmProductOccurrenceData, pod);
  if (A3D_SUCCESS != A3DAsmProductOccurrenceGet(ntt, &pod) ) {
    返回;
  }
  孩子 = EntityArray( pod.m_ppPOccurrences, pod.m_ppPOccurrences +
  pod.m_uiPOOccurrencesSize );
  如果(汽车零件= pod.m_pPart?pod.m_pPart:getPart(pod.m_pPrototype)){
    children.insert(children.begin(), part);
  }
  A3DAsmProductOccurrenceGet( nullptr , &pod);
}
我们已经完成了零件定义!所以让我们在子句中添加部分定义遍历:
} else  if ( kA3DTypeAsmPartDefinition == type ) {
  A3DAsmPartDefinitionData pdd;
  A3D_INITIALIZE_DATA(A3DAsmPartDefinitionData, pdd);
  if (A3D_SUCCESS != A3DAsmPartDefinitionGet(ntt, &pdd) ) {
    返回;
  }
 children = EntityArray(pdd.m_ppRepItems,pdd.m_ppRepItems +
  pdd.m_uiRepItemsSize );
  A3DAsmPartDefinitionGet( nullptr , &pdd);
将我们带到表示项目上,我们应该在其中调用回调函数,提供用于将我们带到这里的路径。但在我们这样做之前,我们不能忘记作为集合的特定表示项类型。如果遇到这种对象类型,我们必须进一步遍历。
处理所有这些细节应该看起来像这样,作为条件的最后一个 else 子句:
 否则{
  如果(kA3DTypeRiSet == 类型){
    A3DRiSetData risd;
    A3D_INITIALIZE_DATA(A3DRiSetData, risd);
    if (A3D_SUCCESS != A3DRiSetGet(ntt, &risd) ) {
      返回;
    }
    children = EntityArray(risd.m_ppRepItems, risd.m_ppRepItems + risd.m_uiRepItemsSize);
    A3DRiSetGet( nullptr , &risd);
  }其他{
    fcn(路径);
  }
}
如果您现在感觉有点头晕,请不要担心,这是完全正常的。我们一起成功地实现了一个行为良好的函数,用于以对我们非常有用的方式遍历 Exchange 产品结构。通过使用函数对象,我们将遍历与构建场景图的工作分开。在此过程中,您可能已经对 Exchange 的数据结构有所了解。

第 3 步:级联属性
让我们继续实现我们在步骤 1 中创建场景时使用的每个函数。我们遇到的下一个存根函数是 lambda 内部的CascadedAttributes结构。此结构在文件CascadedAddtributes.h中实现。打开它看看。您将找到一个空的构造函数和析构函数,我们现在将实现它们。
构造函数有一个参数,你现在应该很熟悉了。它是一个 EntityArray,表示从模型文件到我们感兴趣的表示项的 Exchange 对象的路径。我们的构造函数的工作是计算A3DMiscCascadedAttributesData与该路径对应的对象。我们将按照此处的编程指南关于级联属性的部分提供的指导来执行此操作。
实现构造函数如下:
// 创建一个向量来保存级联属性句柄
QVector<A3DMiscCascadedAttributes*> cascaded_attribs;
// 创建“根”级联属性句柄
cascaded_attribs.push_back( nullptr );
A3DMiscCascadedAttributesCreate( &cascaded_attribs.back() );
// 对于路径中的每个实体,
对于(自动ntt:路径){
  如果(A3DEntityIsBaseWithGraphicsType(ntt)){
    // 获取之前级联属性的句柄
    自动父亲 = cascaded_attribs.back();

// 为这个实体创建一个新的级联属性句柄

    cascaded_attribs.push_back( nullptr );
    A3DMiscCascadedAttributesCreate( &cascaded_attribs.back() );

    // 将此句柄压入堆栈
    A3DMiscCascadedAttributesPush( cascaded_attribs.back(), ntt, 父亲);
  }
}
// 计算级联属性数据
A3D_INITIALIZE_DATA(A3DMiscCascadedAttributesData, d);
A3DMiscCascadedAttributesGet( cascaded_attribs.back(), &d );
对于(自动属性:cascaded_attribs){
    A3DMiscCascadedAttributesDelete(attrib);
}
代码中的注释应该合理地解释方法是什么。
一旦构造了这个对象,我们就适当地填充了数据字段。剩下要做的就是释放析构函数中的对象。将这行代码添加到析构函数中:
A3DMiscCascadedAttributesGet( nullptr , &d);
仅此而已。
完成此步骤意味着您已经创建了一个简单的结构来管理任意 EntityArray 的级联属性。这与我们工作流程的其余部分很好地结合在一起,并直接利用了我们实现的方法来遍历产品结构。

第 4 步:创建网格
在下一步中,我们将介绍从 HOOPS Exchange 读取曲面细分所需的代码,并创建Qt3D适合渲染的相应对象。这项工作将在文件中完成Mesh.cpp。现在在你的编辑器中打开它,你会发现熟悉的 stubbed out 实现。
要开始这项任务,我们应该对传入的句柄执行一些健全性检查。具体来说,我们要确保它是我们要为这个基本查看工作流处理的正确的具体对象类型。
A3DEEntityType tess_type = kA3DTypeUnknown;
if (A3D_SUCCESS != A3DEntityGetType( tess_base, &tess_type ) ) {
  返回 空指针;
}
// 确保我们只处理我们关心的类型
如果(苔丝类型!= kA3DTypeTess3D){
  返回 空指针;
}
传递给函数的句柄是一个名为的基类型A3DTessBase.对于这个基本的查看工作流,我们将只处理具体类型A3DTess3D.如果传入一个空句柄,此代码将正确处理它并退出。
基本镶嵌类型包含我们需要的所有派生类型共有的信息,特别是坐标数组。添加代码以从 HOOPS Exchange 读取基础数据。
// 从 tess 基础数据中读取坐标数组
A3DTessBaseData 待定;
A3D_INITIALIZE_DATA(A3DTessBaseData,待定);
if ( A3D_SUCCESS != A3DTessBaseGet( tess_base, &tbd ) ) {
  返回 空指针;
}
A3DDouble const *coords = tbd.m_pdCoords;
A3DUns32 const n_coords = tbd.m_uiCoordSize;
坐标数据以 C 样式数组的形式提供 - 也就是说,它是一个指向指定长度的双精度数组的指针。大小总是能被 3 整除。
下一个任务是获取与具体细分类型相关的数据。我们将从获取法线向量的 C 样式数组开始。
3DTess3D数据 t3dd;
A3D_INITIALIZE_DATA(A3DTess3DData, t3dd);
if ( A3D_SUCCESS != A3DTess3DGet( tess_base, &t3dd ) ) {
A3DTessBaseGet( nullptr , &tbd);
  返回 空指针;
}
A3DDouble const *normals = t3dd.m_pdNormals;
A3DUns32 const n_normals = t3dd.m_uiNormalSize;
还存储在对象A3DTess3DData数组中A3DTessFaceData,每个拓扑面在精确几何表示中一个。现在我们有了坐标和法线向量的数组,我们可以遍历面部数据并解释其中引用的镶嵌。当我们遍历面时,我们将构建一个包含位置和法线向量的单个 Qt 缓冲区,以及一个简单的“扁平化”索引数组。
每个实例都A3DTessFaceData包含一个位标志字段,用于描述三角形数据的存储方式。通过使用 HOOPS Exchange 生成曲面细分,我们可以合理地确保只有基本三角形存在,因此我们不必担心在从输入文件本身。我们通过生成曲面细分对性能造成了影响,但好处是用于读取生成的数据的简化代码块。
这是从 HOOPS Exchange 读取三角形数据的循环。它交错三角形顶点位置及其法线向量,这通常在可视化工作流程中使用的顶点缓冲区对象中完成。
QVector<quint32> q_indices;
QByteArray 缓冲区字节;
quint32 const stride = sizeof (float) * 6; // 3 表示顶点 + 3 表示法线
对于(自动tess_face_idx = 0u; tess_face_idx < t3dd.m_uiFaceTessSize; ++tess_face_idx ) { A3DTessFaceData const &d = t3dd.m_psFaceTessData[tess_face_idx];
  自动sz_tri_idx = 0u;
  自动ti_index = d.m_uiStartTriangulated;
  if (kA3DTessFaceDataTriangle & d.m_usUsedEntitiesFlags) {
    auto  const num_tris = d.m_puiSizesTriangulated[sz_tri_idx++];
    自动 常量pt_count = num_tris * 3; // 每个三角形 3 分
    auto  const old_sz = bufferBytes.size();
    bufferBytes.resize(bufferBytes.size() + stride * pt_count);
    auto fptr = reinterpret_cast< float * > (bufferBytes.data() + old_sz);
    对于(自动三= 0u;三<num_tris;三++){
    对于(自动垂直= 0u;垂直<3u;垂直++){
      自动 常量&normal_index =
      t3dd.m_puiTriangulatedIndexes[ti_index++];
      自动 常量&coord_index =
      t3dd.m_puiTriangulatedIndexes[ti_index++];
      *fptr++ = coords[coord_index];
      *fptr++ = coords[coord_index+1];
      *fptr++ = coords[coord_index+2];
      *fptr++ = normals[normal_index];
      *fptr++ = normals[normal_index+1];
      *fptr++ = normals[normal_index+2];
      q_indices.push_back(q_indices.size());
      }
    }
  }
}
当这个循环结束时,我们留下一个原始缓冲区,其中包含身体中每个三角形的浮点顶点位置和法线向量。它们按顺序存储,不考虑共享索引值的可能性。这导致缓冲区可能比需要的更大,但简化了我们呈现的代码。
我们从 Exchange 获得了我们需要的所有数据,所以让我们自己清理一下。
A3DTess3DGet( nullptr , &t3dd);
A3DTessBaseGet( nullptr , &tbd);
我们必须通过创建Qt3D渲染刚刚捕获的数据所需的原语来完成该功能。正如本教程开头所提到的,我们不会花太多时间来描述细节,Qt3D,而是根据需要呈现代码:
auto buf = new Qt3DCore::QBuffer();
buf->setData(bufferBytes);
自动几何=新的QGeometry;
auto position_attribute = new QAttribute(buf,
QAttribute::defaultPositionAttributeName(), QAttribute::Float, 3, q_indices.size(), 0, stride);
几何->addAttribute(位置属性);
auto normal_attribute = new QAttribute( buf,
QAttribute::defaultNormalAttributeName(), QAttribute::Float, 3, q_indices.size(), sizeof (float) * 3, stride );
几何->addAttribute( normal_attribute );
QByteArray indexBytes;
QAttribute::VertexBaseType ty;
如果(q_indices.size() < 65536) {
  // 我们可以使用 USHORT
  ty = QAttribute::UnsignedShort;
  indexBytes.resize(q_indices.size() * sizeof (quint16));
  quint16 *usptr = reinterpret_cast< quint16* > (indexBytes.data());
  for ( int i = 0; i < int(q_indices.size()); ++i)
    *usptr++ = static_cast<quint16>(q_indices.at(i));
}其他{
  // 使用 UINT - 不需要转换,但让我们确保 int 是 32 位的!
  ty = QAttribute::UnsignedInt;
  Q_ASSERT( sizeof ( int ) == sizeof (quint32));
  indexBytes.resize(q_indices.size() * sizeof (quint32));
  memcpy(indexBytes.data(), reinterpret_cast< const char * > (q_indices.data()), indexBytes.size());
}
自动*indexBuffer = new Qt3DCore::QBuffer(); indexBuffer->setData(indexBytes);
QAttribute *indexAttribute = new QAttribute(indexBuffer, ty, 1, q_indices.size());
indexAttribute->setAttributeType(QAttribute::IndexAttribute);
几何->addAttribute(indexAttribute);
自动渲染器 =新Qt3DRender::QGeometryRenderer();
渲染器->setGeometry(几何);
返回渲染器
完成此步骤后,您已达到一个重要里程碑。现在,您可以加载单个零件并查看它。它将以默认颜色(红色)显示,但应该是可见的。程序集无法正确显示,因为我们尚未处理转换,但加载示例文件 samples/data/prc/Flange287. prc,您应该看到以下内容:
接下来,我们将专注于使转换正确,以便我们可以正确地可视化程序集。

第 5 步:创建转换
现在我们在屏幕上有了一些东西,让我们添加在世界中正确定位对象所需的代码。完成后,我们将能够加载和查看程序集。
在程序集文件中,程序集树的各个节点包含本地转换。每个变换都相对于其父级应用。这意味着,要计算每个零件的世界变换,我们必须在通向零件实例的路径中累积每个装配节点的变换。
根据这个描述,我们可以开始编写 createTransform(在 Transform.cpp 中找到)的实现,如下所示:
QMatrix4x4 网络矩阵;
对于(自动 常量ntt:路径){
  A3DMiscTransformation *xform = getTransform(ntt);
  net_matrix *= toMatrix( xform );
}
自动xform =新Qt3DCore::QTransform();
xform->setMatrix(net_matrix);
返回xform;
这个实现完全按照我们所描述的方便的事实来描述,路径包括指向表示项的程序集层次结构中每个对象的顺序句柄列表。它使用了两个我们仍然必须定义的函数,getTransform我们toMatrix.将在上面的匿名命名空间中实现它们createTransform.
我们getTransform.将从它的用法开始,这个函数接受一个实体句柄并返回一个A3DMiscTransformation句柄。我们必须实现这个函数来确定传入的实体的类型,并从它返回转换(如果存在)。
在从模型文件到表示项的路径中,唯一可能包含转换的对象类型是A3DAsmProductOccurrence和A3DRiRepresentationItem.我们的代码必须处理这两种情况。实现getTransform功能如下:
命名空间{
    A3DMiscTransformation *getTransform( A3DEntity *ntt ) {

        A3DMiscTransformation *result = nullptr ;

        A3DEEntityType ntt_type = kA3DTypeUnknown;
        A3DEntityGetType(ntt, &ntt_type );
        if ( kA3DTypeAsmProductOccurrence == ntt_type ) {
            A3DAsmProductOccurrenceData d;
            A3D_INITIALIZE_DATA(A3DAsmProductOccurrenceData, d);
            A3DAsmProductOccurrenceGet(ntt, &d);
            结果 = d.m_pLocation ?d.m_pLocation:getTransform(d.m_pPrototype);
            A3DAsmProductOccurrenceGet( nullptr , &d);
        } else  if (ntt_type > kA3DTypeRi && ntt_type <= kA3DTypeRiCoordinateSystemItem) {
            A3DRiRepresentationItemData d;
            A3D_INITIALIZE_DATA(A3DRiRepresentationItemData, d);
            A3DRiRepresentationItemGet(ntt, &d);
            如果(自动ti_cs = d.m_pCoordinateSystem){
                A3DRiCoordinateSystemData cs_d;
                A3D_INITIALIZE_DATA(A3DRiCoordinateSystemData, cs_d);
                A3DRiCoordinateSystemGet(d.m_pCoordinateSystem, &cs_d);
                结果 = cs_d.m_pTransformation;
                A3DRiCoordinateSystemGet( nullptr , &cs_d);
            }
            A3DRiRepresentationItemGet( nullptr , &d);
        }
        返回结果;
    }
}
在这个实现中有两个值得注意的地方。也许你已经发现了它们。
首先,在 if 子句中,kA3DTypeAsmProductOccurrence,您可能已经注意到选项结果的三元运算符。如果为空,getTransform则使用原型指针递归调用。m_pLocation这是因为装配节点在未被覆盖时会从其原型“继承”位置字段。
第二个注释在 else if 条件本身中。因为A3DEntityGetType返回提供的实体的具体类型,所以我们必须使用这里介绍的逻辑来查看实体是否是所有可能的表示项类型中的任何一种。不幸的是,它依赖于枚举值。我愿意接受有关处理此问题的更好方法的建议(ExchangeToolkit.h有一个名为 的函数isRepresentationItem)。
有了A3DMiscTransformation句柄,我们现在准备实现 toMatrix,它必须将句柄转换为 aQMatrix4x4. A3DMiscTranformation是具有两种可能的具体类型的基类句柄:A3DMiscCartesianTransformation我们A3DMiscGeneralTransformation.必须处理这两种情况。为此,请使用以下代码在匿名命名空间的顶部创建函数:
QMatrix4x4 toMatrix(A3DMiscTransformation *xfrm){
  如果(xfrm){
    A3DEEntityType xfrm_type = kA3DTypeUnknown;
    A3DEntityGetType(xfrm, &xfrm_type);
    开关(xfrm_type){
      案例kA3DTypeMiscCartesianTransformation:
        返回getMatrixFromCartesian(xfrm);
        休息;
      案例kA3DTypeMiscGeneralTransformation:
        返回getMatrixFromGeneralTransformation(xfrm);
        休息;
      默认:
        throw std::invalid_argument( "意外类型。" );
        休息;
    }
  }
  返回QMatrix4x4();
}
一般变换将其矩阵表示为代表 4x4 矩阵的 16 元素双精度数组。QMatrix4x4将这些值复制到对象中很简单。在匿名命名空间的顶部创建以下函数来处理这种情况。
QMatrix4x4 getMatrixFromGeneralTransformation(A3DMiscGeneralTransformation *xform){
  A3DMiscGeneralTransformationData d;
  A3D_INITIALIZE_DATA(A3DMiscGeneralTransformationData, d);
  A3DMiscGeneralTransformationGet(xform, &d);

  自动 常数系数 = d.m_adCoeff;
  QMatrix4x4 结果;
  for (自动行 = 0u; 行 < 4u; ++row ) {
    对于(自动col = 0u;col < 4u;++col){
      结果(row,col) = static_cast< float > (coeff[row + col * 4]);
    }
  }
返回结果;
处理笛卡尔变换的情况要复杂一些。我们必须读取基本数据并执行一些元素代数来计算矩阵的值。将此代码添加到匿名命名空间以提取笛卡尔变换数据。
QMatrix4x4 getMatrixFromCartesian(A3DMiscCartesianTransformation *xform){
  A3DMiscCartesianTransformationData d;
  A3D_INITIALIZE_DATA(A3DMiscCartesianTransformationData, d);
  A3DMiscCartesianTransformationGet(xform, &d);
  auto  const mirror = (d.m_ucBehaviour & kA3DTransformationMirror) ?-1。: 1.;
  auto  const s = toQVector3D(d.m_sScale);
  auto  const o = toQVector3D(d.m_sOrigin);
  auto  const x = toQVector3D(d.m_sXVector);
  auto  const y = toQVector3D(d.m_sYVector);
  auto  const z = QVector3D::crossProduct( x, y ) * mirror;
  A3DMiscCartesianTransformationGet( nullptr , &d);
  返回QMatrix4x4(
    xx() * sx(), yx() * sy(), zx() * sz(), ox(),
    xy() * xx(), yy() * sy(), zy() * sz(), oy(),
    xz() * sx(), yz() * sy(), zz() * sz(), oz(),
    0.f, 0.f, 0.f, 1.f
  );
}
此代码使用从对象toQVector3D创建 a的函数。它在Transform.h中实现。QVector3DA3DVector3DData
添加此功能后,您将拥有一个完整的实现以供测试。运行您的应用程序并加载一个程序集文件,例如data/prc/_micro engine.prc。

第 6 步:创建材料
本教程的最后一步是创建代表我们从 Exchange 读取的样式数据的 Qt3D 材质。要确定零件的外观,我们必须依赖从第 3 步的级联属性助手中检索到的数据。回想一下,可见性是由通过装配的特定路径决定的。应以相同的方式计算应绘制的部分样式。在createScene,我们调用函数的主体中,createMaterial并从我们的级联属性助手中传递样式数据。

打开文件材料。cpp 这样我们就可以开始实现该功能了。您将看到创建了默认材质,这就是所有部件都显示为红色的原因。传入此函数的样式数据对象可以通过 3 种不同的方式指定材质信息。最简单的情况是单色。让我们从处理那个案例开始。
更新函数如下:
Qt3DCore::QComponent *createMaterial( A3DGraphStyleData const &style_data ) {
  自动材质 =新Qt3DExtras::QDiffuseSpecularMaterial();
  材料->setDiffuse(QColor(“红色”));
  如果(!style_data.m_bMaterial){
    auto  const a = style_data.m_bIsTransparencyDefined ?style_data.m_ucTransparency:255u;
    材料->setDiffuse(getColor(style_data.m_uiRgbColorIndex, a));
  }
  退回材料;
}
在这里,我们使用了一个我们仍然必须实现的getColor.函数,这个函数接受一个 RGB 颜色索引(和 alpha)并在上面的匿名命名空间中返回一个QColor.实现getColorcreateMaterial.

命名空间{
  QColor getColor(A3DUns32 const &color_idx, int  const &a) {
    如果(A3D_DEFAULT_COLOR_INDEX == color_idx){
      返回QColor( 255, 0, 0 );
    }
    A3DGraphRgbColorData rgb_color_data;
    A3D_INITIALIZE_DATA(A3DGraphRgbColorData, rgb_color_data);
    A3DGlobalGetGraphRgbColorData(color_idx, &rgb_color_data);
    自动 常量&r = rgb_color_data.m_dRed;
    自动 常数&g = rgb_color_data.m_dGreen;
    自动 常量&b = rgb_color_data.m_dBlue;
    返回QColor( static_cast<int>(r * 255), static_cast<int>(g * 255), static_cast<int>(b * 255), a);
  }
}
颜色数据通过整数索引存储在 HOOPS Exchange 中。这个实现首先检查索引是否等于A3D_DEFAULT_COLOR_INDEX,表示没有分配颜色。在这种情况下,我们返回红色,你会认为这是我最喜欢的颜色,但你错了。从 Exchange 的双精度定义创建QColor对象是一件简单的事情,自然而然。
通过此实现,您会发现许多部件现在将加载并以正确的颜色显示。
让我们添加一个额外的案例来处理样式数据可以采用的两种或三种形式。使用以下 else 块更新 createMaterial 中的 if 子句。
否则{
  A3DBool is_textuture = false ;
  A3DGlobalIsMaterialTexture(style_data.m_uiRgbColorIndex, &is_texuture);
  如果(!is_textuture){
    A3DGraphMaterialData material_data;
    A3D_INITIALIZE_DATA(A3DGraphMaterialData, material_data);
    A3DGlobalGetGraphMaterialData(style_data.m_uiRgbColorIndex, &material_data);
    auto  constambient_color = getColor(material_data.m_uiAmbient, static_cast<int>(255 * material_data.m_dAmbientAlpha));
    auto  constdiffuse_color = getColor(material_data.m_uiDiffuse, static_cast<int>(255 * material_data.m_dDiffuseAlpha));
    if (ambient_color.alpha() == 255 &&diffuse_color.alpha() == 0) {
    材料->setDiffuse(ambient_color);
    }否则 if (ambient_color.alpha() == 0 &&diffuse_color.alpha() == 255) {
        材料->setDiffuse(diffuse_color);
    }
    材质->setSpecular(getColor(material_data.m_uiSpecular,material_data.m_dSpecularAlpha));
  }
}
这可以处理稍微复杂的材质定义。处理纹理超出了本基本查看教程的范围。我们已经处理了两种最常见的样式定义情况,并且我们正在返回一个合理的 Qt3D 材料。

结论

恭喜!您已经完成了涵盖基本查看工作流程的非常详细的教程。在此过程中,您了解了 HOOPS Exchange 装配结构及其对零件显示方式的影响。您学习了如何阅读曲面细分的基本形式并将其解释为一种常见的基于缓冲区的查看技术。我们通过读取变换数据将对象放置在正确的位置和方向上,最后为每个部分应用合理的材料,使它们看起来像预期的那样。

了解HOOPS技术详情欢迎进入HOOPS中文网

申请HOOPS试用


慧都科技是HOOPS全套产品中国地区的指定经销商,提供HOOPS售卖、HOOPS 60天的免费试用、中文技术支持,同时提供工业3D解决方案,如果您对此感兴趣,欢迎电话咨询:023-68661681

↓ ↓ 关注“HOOPS技术”微信公众号,了解HOOPS技术的真实应用 ↓ ↓