4. Mesh Viewer

Introduction

This tutorial will take you through the process of utilizing HOOPS Exchange’s enhanced API to achieve a basic CAD model viewing experience.

This guide will walk you through the necessary steps to retrieve mesh coordinates and topology, empowering you to establish a solid foundation for rendering and navigating CAD models within your applications. By the end of the tutorial, you’ll confidently present CAD models with precision, setting the stage for exploring advanced functionalities.

../_images/basic_viewing.png

Code Overview

The code we will be writing in this document first loads a model file using A3DAsmModelFileLoadFromFile(). Once loaded, we traverse the model file from the root to the representation item where we use A3DRiComputeMesh() to obtain a mesh from each of them. This function returns an A3DMeshData that we will use to build OpenGL Buffer Objects. For the sake of simplicity, this tutorial will focus on vertex coordinates and normals only.

The rest of the code uses OpenGL and GLFW to provide a window and display the geometry on screen.

Prerequisites

Before you start, ensure you have:

Preparing the Environment

Before we start coding, we need to prepare the project environment.

First, create a folder called basic_viewer. This folder will contain your project configuration, your code, and the dependencies it uses.

Adding Dependencies

While HOOPS Exchange serves as our tool for loading and manipulating tessellation data, the task of displaying it in a 3D scene is left to the renowned OpenGL API. Consequently, our project will require a few essential dependencies:

GLFW

GLFW is a multi-platform library that we will use for creating our window and communicating with the OpenGL API. In this tutorial, we will be taking GLFW directly from the sources, but GLFW is provided in many ways. Click here to download GLFW 3.3.8. Once the package is downloaded, extract it and move its contents to the root of your folder basic_viewer.

Glad

Since we will be using OpenGL 4.2 for this tutorial, we want to ensure your environment has access to all OpenGL functions for this version. As such, we will be using the Glad loader which conveniently fits well with the GLFW ecosystem. Go to this page and click on the Generate button (don’t edit the form). On the next page, download glad.zip. Once the package is downloaded, extract and place all the files at the root of your project.

linmath.h

linmath.h is a single header-only dependency that we will use for basic math operations. Go to the project page, and download the file linmath.h. Place the file at the root of your project.

Base Source Code

We will start with a simple code that initializes HOOPS Exchange. At the root of your project, create a file called main.cpp and fill it with the following code:

 1#include <cassert>
 2#include <cstdio>
 3#include <cstdlib>
 4
 5#define INITIALIZE_A3D_API
 6#include <A3DSDKIncludes.h>
 7
 8int main(int argc, char* argv[])
 9{
10    A3DBool loaded = A3DSDKLoadLibraryA("/path/to/exchange");
11    assert(loaded);
12
13    A3DStatus result = A3DLicPutUnifiedLicense(HOOPS_LICENSE);
14    assert(result == A3D_SUCCESS);
15
16    result = A3DDllInitialize(A3D_DLL_MAJORVERSION, A3D_DLL_MINORVERSION);
17    assert(result == A3D_SUCCESS);
18
19    printf("HOOPS Exchange is ready!\n");
20
21    A3DDllTerminate();
22
23    // Unload the library
24    A3DSDKUnloadLibrary();
25
26    return EXIT_SUCCESS;
27}

For more details on how to initialize HOOPS Exchange, see Initializing HOOPS Exchange.

Configuring CMake

At the root of your project, create a file called CMakeLists.txt and fill it with the following code:

 1cmake_minimum_required(VERSION 3.18)
 2project(MeshViewer C)
 3
 4set(GLFW_BUILD_EXAMPLES OFF)
 5set(GLFW_BUILD_TESTS OFF)
 6set(GLFW_BUILD_DOCS OFF)
 7add_subdirectory(glfw-3.3.8)
 8
 9set(EXCHANGE_PACKAGE_PATH "" CACHE PATH "Path to Exchange")
10if(NOT EXCHANGE_PACKAGE_PATH)
11    message(FATAL_ERROR "EXCHANGE_PACKAGE_PATH must be set to a valid folder path.")
12endif()
13
14add_executable(MeshViewer main.cpp glad.c)
15target_include_directories(MeshViewer PRIVATE
16    "${EXCHANGE_PACKAGE_PATH}/include"
17    "${CMAKE_CURRENT_SOURCE_DIR}"
18)
19target_link_libraries(MeshViewer PRIVATE glfw)

This CMake file is similar to the one provided in Setting Up a CMake Project with the addition of the above-mentioned libraries.

Check Your Environment

At the end of this configuration, your folder should look like this:

basic_viewer/
├── glfw-3.3.8
│   ├── include
│   ├── src
│   ├── CMakeLists.txt
│   └── ...
├── CMakeLists.txt
├── glad.c
├── glad.h
├── khrplatform.h
├── linmath.h
└── main.cpp

Go to Compile and Run the Project to test your environment. At this point of the tutorial, running your executable should display “HOOPS Exchange is ready!” in your standard output.

Load the Model File

First, we will create a function that loads a model file passed in parameter and return the result. In case of error, the model file is not created and the function returns a null handle:

 1A3DAsmModelFile* load_model_file(const char* path)
 2{
 3    A3DAsmModelFile*    model_file                           = 0;
 4    A3DRWParamsLoadData load_params;
 5    A3D_INITIALIZE_DATA(A3DRWParamsLoadData, load_params);
 6    load_params.m_sGeneral.m_bReadSolids                     = A3D_TRUE;
 7    load_params.m_sAssembly.m_bUseRootDirectory              = A3D_TRUE;
 8    load_params.m_sAssembly.m_bRootDirRecursive              = A3D_TRUE;
 9    load_params.m_sGeneral.m_eReadGeomTessMode               = kA3DReadGeomAndTess;
10    load_params.m_sTessellation.m_eTessellationLevelOfDetail = kA3DTessLODHigh;
11    load_params.m_sGeneral.m_bReadSurfaces                   = A3D_TRUE;
12    load_params.m_sMultiEntries.m_bLoadDefault               = A3D_TRUE;
13
14    code = A3DAsmModelFileLoadFromFile(path, &load_params, &model_file);
15    if (code  != A3D_SUCCESS) {
16        printf("Error %d while loading '%s'\n", code, path);
17    }
18    return model_file;
19}

This function follows the usual loading workflow of a model file with no particularities. Note that m_sTessellation::m_eReadGeomTessMode is set to kA3DReadGeomAndTess.

This function returns the handle to the loaded model file. The function will be called from main().

Now that we have our file loaded, let’s initialize our graphics context.

Initialize the Graphics Context

This operation has been delegated to OpenGL and GLFW. For the latter, we define glfw_prepare(). This function creates a new window, specifies a few callback functions, and initializes the OpenGL context. It also returns the handle to our windows, which we will use later on.

 1GLFWwindow* glfw_prepare()
 2{
 3    glfwSetErrorCallback(glfw_error_callback);
 4
 5    if (!glfwInit())
 6        exit(EXIT_FAILURE);
 7
 8    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
 9    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
10    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
11
12    GLFWwindow* window = glfwCreateWindow(800, 600, "MeshViewer", NULL, NULL);
13    if (!window)
14    {
15        glfwTerminate();
16        exit(EXIT_FAILURE);
17    }
18
19    glfwSetKeyCallback(window, glfw_key_callback);
20
21    glfwMakeContextCurrent(window);
22    gladLoadGL();
23    glfwSwapInterval(1);
24
25    return window;
26}

glfw_error_callback() and glfw_key_callback() must also be defined:

1void glfw_error_callback(int error, const char* description)
2{
3    fprintf(stderr, "Error: %s\n", description);
4}
1void glfw_key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
2{
3    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
4        glfwSetWindowShouldClose(window, GLFW_TRUE);
5}

Preparing OpenGL is mainly about compiling the shader program. In gl_prepare_program(), we write our programs and compile them. The function then stores the name of some vertex attributes for later purposes and returns the program that we will be using later on:

 1GLuint gl_prepare_program()
 2{
 3    glEnable(GL_DEPTH_TEST);
 4    glEnable(GL_CULL_FACE);
 5    glCullFace(GL_BACK);
 6
 7    const char* vertex_shader_text =
 8   "#version 420 core\n"
 9   "layout (location = 0) in vec3 vPos;\n"
10   "layout (location = 1) in vec3 vNorm;\n"
11   "out vec3 FragNormal;\n"
12   "uniform mat4 M;\n"
13   "uniform mat4 V;\n"
14   "uniform mat4 P;\n"
15   "void main()\n"
16   "{\n"
17       "gl_Position = P * V * M * vec4(vPos, 1.0f);\n"
18       "FragNormal = normalize(mat3(transpose(inverse(M))) * vNorm);\n"
19   "}\n";
20
21
22    const char* fragment_shader_text =
23   "#version 420 core\n"
24   "out vec4 FragColor;\n"
25   "\n"
26   "in vec3 FragNormal;\n"
27     "\n"
28   "void main()\n"
29   "{\n"
30       "vec3 normal = normalize(FragNormal);\n"
31       "vec3 lightDirection = normalize(vec3(1.0, 1.0, 1.0));\n"
32       "float diff = max(dot(normal, lightDirection), 0.0);\n"
33       "vec4 diffuseColor = vec4(0.7, 0.7, 0.7, 1.0);\n"
34       "FragColor = diff * diffuseColor;\n"
35   "}\n";
36
37    GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);
38    glShaderSource(vertex_shader, 1, &vertex_shader_text, NULL);
39    glCompileShader(vertex_shader);
40
41    GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
42    glShaderSource(fragment_shader, 1, &fragment_shader_text, NULL);
43    glCompileShader(fragment_shader);
44
45    GLuint program = glCreateProgram();
46    glAttachShader(program, vertex_shader);
47    glAttachShader(program, fragment_shader);
48    glLinkProgram(program);
49
50    glDeleteShader(vertex_shader);
51    glDeleteShader(fragment_shader);
52
53    return program;
54}

Now the two functions can be called from main() with:

GLFWwindow* window  = glfw_prepare();
GLuint      program = gl_prepare_program(&gl_scene);

Don’t forget to add new requirements at the top of the file:

#include <glad.h>
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>

Custom Data

While traversing the model file and building the buffer objects, we will be accumulating data that we need for later purposes. Our code then needs two structures.

SceneObject represents a drawable object in our OpenGL scene. It consists of the Vertex Array Object (VAO) it is made of as well as the number of vertex indices the Vertex Buffer Object (VBO) has. On top of that, a transform matrix is provided to place the model in space.

A SceneObject is created for each drawable element in our PRC while a Vertex Array Object will be created once for each Representation Item. It is not uncommon in CAD that an element is present more than one time in a scene, but with different positioning (for example, bolts or screws). Thus, we may have distinct SceneObject instances that contain the same Vertex Array Object identifier.

1typedef struct {
2    mat4x4  mat_transform_model;
3    GLuint  gl_vao;
4    GLsizei gl_indices_count;
5} SceneObject;

SceneObject’s are created one by one, for each resulting A3DMeshData or A3DRiComputeMesh(). Then, all along traversal, it is stored in a TraverseData with other fields.

1typedef struct {
2    std::vector<SceneObject> objects;
3
4    std::unordered_map<A3DRiRepresentationItem*, std::pair<GLuint, GLsizei>> ri_to_gl;
5    std::vector<GLuint> gl_vaos;
6    std::vector<GLuint> gl_vbos;
7} TraverseData;

The TraverseData structure is used to keep track of important information while traversing the model file.

In addition to objects that is the actual result of the traversal, ri_to_gl is an associative map that links representation items to their respective VAO identifier. This is used during traversal to avoid recreating a buffer objects set for representation items we already have one.

gl_vaos and gl_vbos are cleanup resources. They store all the VAO and VBO identifiers to delete them properly at the end of our main() function.

The two structures above add the following requirements at the top of our code:

#include <unordered_map>
#include <utility>
#include <vector>

Creating the Buffer Objects

This function is crucial for this tutorial as it marks the moment when we combine HOOPS Exchange structures with OpenGL. In this context, we use the A3DMeshData to construct the lists of coordinates, normals, and indices required to build our OpenGL buffer objects.

The gl_make_buffers() function takes an A3DMeshData as a parameter and processes the vertex and normal coordinates to construct an OpenGL-ready buffer. The function returns a pair consisting of a VAO (Vertex Array Object) identifier and the number of indices in the mesh. It also takes a TraverseData object as an argument to keep track of OpenGL objects that will be deleted at the end of our program.

std::pair<GLuint, GLsizei> gl_make_buffers(A3DMeshData* const data_mesh, TraverseData* const data_traverse)
{
     // Code
}

Converting the Mesh Data

A difference exists between how A3DMeshData and OpenGL organize indices in memory. While A3DMeshData uses separate lists of indices for each vertex attribute (one for coordinates and another for normals), OpenGL requires a single set of indices that reference both the coordinates and normals. Therefore, our first step is to convert the mesh data so that both vertex attributes share the same indices.

We create two std::vector’s to store the final buffer data before sending it to the GPU:

3std::vector<GLdouble> vertex_buffer;
4std::vector<GLuint>   index_buffer;

The vertex_buffer will hold both the vertex coordinates and normals interleaved in this layout:

vertex_buffer = {
     // Coordinates         , Normals
     Vx0, Vy0, Vz0, Nx0, Ny0, Nz0, // Vertex 0
     Vx1, Vy1, Vz1, Nx1, Ny1, Nz1, // Vertex 1
     Vx2, Vy2, Vz2, Nx2, Ny2, Nz2, // Vertex 2
     ...
};

This interleaving is not necessary, but since we are already reorganizing the index buffer data from A3DMeshData, this is an opportunity to use a single VBO (Vertex Buffer Object) identifier for vertex attributes instead of two.

The index_buffer will contain the list of all indices that reference the vertex attributes in the vertex_buffer. The indices are ordered considering we will render triangles.

The first thing we need to do is determine the total number of indices in our mesh. For this, we iterate through A3DMeshData::m_puiTriangleCountPerFace, where each value corresponds to the number of triangles in a face. Since each triangle has 3 vertices, our total number of indices is the sum of three times each value in the array:

 8size_t n_indices = 0;
 9for (A3DUns32 face_i = 0; face_i < data_mesh->m_uiFaceSize; ++face_i) {
10    n_indices += 3 * data_mesh->m_puiTriangleCountPerFace[face_i];
11}

n_indices will eventually be the size of index_buffer.

Our next algorithm does the following:

  • It iterates through A3DMeshData::m_ppuiPointIndicesPerFace and A3DMeshData::m_ppuiNormalIndicesPerFace simultaneously to obtain the coordinate and normal indices.

  • For each pair of indices, it retrieves the X, Y, and Z coordinates from A3DMeshData::m_pdCoords and the X, Y, and Z normals from A3DMeshData::m_pdNormals. All six values are appended to the vertex_buffer.

  • This operation creates a new index to add to the index_buffer.

To prevent memory redundancy, we also identify every unique pair of coordinate/normal indices and store them in an associative map called index_cache. This way, whenever we encounter the same pair of A3DMeshData indices, we reuse the index from the cache.

The final algorithm looks like this:

15std::unordered_map<uint64_t, GLuint> index_cache;
16
17for (A3DUns32 face_i = 0; face_i < data_mesh->m_uiFaceSize; ++face_i) {
18    A3DUns32 n_triangles = data_mesh->m_puiTriangleCountPerFace[face_i];
19    if (n_triangles == 0) {
20        continue;
21    }
22    const A3DUns32* ptr_coord_index = data_mesh->m_ppuiPointIndicesPerFace[face_i];
23    const A3DUns32* ptr_normal_index = data_mesh->m_ppuiNormalIndicesPerFace[face_i];
24
25    for (size_t vertex_i = 0 ; vertex_i < n_triangles * 3 ; ++vertex_i) {
26        A3DUns32 coord_index = *ptr_coord_index++;
27        A3DUns32 normal_index = *ptr_normal_index++;
28
29        uint64_t cache_key = ((uint64_t)coord_index << 32) | normal_index;
30        auto insertion = index_cache.insert({cache_key, vertex_buffer.size() / 6});
31
32        GLuint vertex_index = insertion.first->second;
33        if (insertion.second) {
34            // Retrieve the coordinates and normals
35            auto x = data_mesh->m_pdCoords[coord_index];
36            auto y = data_mesh->m_pdCoords[coord_index + 1];
37            auto z = data_mesh->m_pdCoords[coord_index + 2];
38
39            // Append the coordinates and normals to the vertex_buffer
40            vertex_buffer.insert(vertex_buffer.end(), {
41                data_mesh->m_pdCoords[coord_index],
42                data_mesh->m_pdCoords[coord_index + 1],
43                data_mesh->m_pdCoords[coord_index + 2],
44                data_mesh->m_pdNormals[normal_index],
45                data_mesh->m_pdNormals[normal_index + 1],
46                data_mesh->m_pdNormals[normal_index + 2]
47            });
48        }
49        // Add the index to the index_buffer
50        index_buffer.push_back(vertex_index);
51    }
52}

Sending the Data to the GPU

The next step in the gl_make_buffers() function is to send the vertex_buffer and index_buffer data to the GPU’s memory, allowing OpenGL to access them during rendering.

We create two Vertex Buffer Objects: gl_bo_vertex, which will contain the vertex attributes (coordinates and normals), and gl_bo_index for the indices. Additionally, we create a Vertex Array Object (VAO) named gl_vao. The VAO is used to enable/disable both gl_bo_vertex and gl_bo_index simultaneously when rendering. Essentially, the entire mesh is referenced using the gl_vao.

The following code creates the buffer objects and populates them with data from vertex_buffer and index_buffer. It also enables various OpenGL states that are tied to our Vertex Array Object:

52GLuint gl_vao = 0;
53glGenVertexArrays(1, &gl_vao);
54glBindVertexArray(gl_vao);
55
56GLuint gl_bo_vertex = 0;
57glGenBuffers(1, &gl_bo_vertex);
58glBindBuffer(GL_ARRAY_BUFFER, gl_bo_vertex);
59glBufferData(GL_ARRAY_BUFFER, vertex_buffer.size() * sizeof(GLdouble), vertex_buffer.data(), GL_STATIC_DRAW);
60
61GLuint gl_bo_index = 0;
62glGenBuffers(1, &gl_bo_index);
63glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gl_bo_index);
64glBufferData(GL_ELEMENT_ARRAY_BUFFER, index_buffer.size() * sizeof(GLuint), index_buffer.data(), GL_STATIC_DRAW);
65
66glEnableVertexAttribArray(GL_SHADER_COORD_LOCATION);
67glVertexAttribPointer(GL_SHADER_COORD_LOCATION, 3, GL_DOUBLE, GL_FALSE, 6 * sizeof(GLdouble), (void*)0);
68glEnableVertexAttribArray(GL_SHADER_NORMAL_LOCATION);
69glVertexAttribPointer(GL_SHADER_NORMAL_LOCATION, 3, GL_DOUBLE, GL_FALSE, 6 * sizeof(GLdouble), (void*)(3 * sizeof(GLdouble)));
70
71glBindVertexArray(0);
72glBindBuffer(GL_ARRAY_BUFFER, 0);
73glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

Now that our GPU data is created, we no longer need vertex_buffer and index_buffer. We add the created buffers to our TraverseData object for later deletion, and gl_make_buffers() returns two values: gl_vao and the size of index_buffer, cast to GLsizei. These values will be used by the caller function.

Here is the full code for gl_make_buffers():

std::pair<GLuint, GLsizei> gl_make_buffers(A3DMeshData* const data_mesh, TraverseData* const data_traverse)
{
    std::vector<GLuint> index_buffer;
    std::vector<GLdouble> vertex_buffer;

    // Count the total number of indices
    // The buffer objects will have at max n_indices indices
    size_t n_indices = 0;
    for (A3DUns32 face_i = 0; face_i < data_mesh->m_uiFaceSize; ++face_i) {
        n_indices += 3 * data_mesh->m_puiTriangleCountPerFace[face_i];
    }

    // This map will serve as a cache for the OpenGL indices
    // vertex_index|normal_index -> gl_index
    std::unordered_map<uint64_t, GLuint> index_cache;

    for (A3DUns32 face_i = 0; face_i < data_mesh->m_uiFaceSize; ++face_i) {
        A3DUns32 n_triangles = data_mesh->m_puiTriangleCountPerFace[face_i];
        if (n_triangles == 0) {
            continue;
        }
        const A3DUns32* ptr_coord_index = data_mesh->m_ppuiPointIndicesPerFace[face_i];
        const A3DUns32* ptr_normal_index = data_mesh->m_ppuiNormalIndicesPerFace[face_i];

        for (size_t vertex_i = 0; vertex_i < n_triangles * 3; ++vertex_i) {
            A3DUns32 coord_index = *ptr_coord_index++;
            A3DUns32 normal_index = *ptr_normal_index++;

            uint64_t cache_key = ((uint64_t)coord_index << 32) | normal_index;
            auto insertion = index_cache.insert({ cache_key, vertex_buffer.size() / 6 });

            GLuint vertex_index = insertion.first->second;
            if (insertion.second) {
                // Retrieve the coordinates and normals
                auto x = data_mesh->m_pdCoords[coord_index];
                auto y = data_mesh->m_pdCoords[coord_index + 1];
                auto z = data_mesh->m_pdCoords[coord_index + 2];

                // Append the coordinates and normals to the vertex_buffer
                vertex_buffer.insert(vertex_buffer.end(), {
                    data_mesh->m_pdCoords[coord_index],
                    data_mesh->m_pdCoords[coord_index + 1],
                    data_mesh->m_pdCoords[coord_index + 2],
                    data_mesh->m_pdNormals[normal_index],
                    data_mesh->m_pdNormals[normal_index + 1],
                    data_mesh->m_pdNormals[normal_index + 2]
                });
            }
            // Add the index to the index_buffer
            index_buffer.push_back(vertex_index);
        }
    }

    GLuint gl_vao = 0;
    glGenVertexArrays(1, &gl_vao);
    glBindVertexArray(gl_vao);

    GLuint gl_bo_vertex = 0;
    glGenBuffers(1, &gl_bo_vertex);
    glBindBuffer(GL_ARRAY_BUFFER, gl_bo_vertex);
    glBufferData(GL_ARRAY_BUFFER, vertex_buffer.size() * sizeof(GLdouble), vertex_buffer.data(), GL_STATIC_DRAW);

    GLuint gl_bo_index = 0;
    glGenBuffers(1, &gl_bo_index);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gl_bo_index);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, index_buffer.size() * sizeof(GLuint), index_buffer.data(), GL_STATIC_DRAW);

    glEnableVertexAttribArray(GL_SHADER_COORD_LOCATION);
    glVertexAttribPointer(GL_SHADER_COORD_LOCATION, 3, GL_DOUBLE, GL_FALSE, 6 * sizeof(GLdouble), (void*)0);
    glEnableVertexAttribArray(GL_SHADER_NORMAL_LOCATION);
    glVertexAttribPointer(GL_SHADER_NORMAL_LOCATION, 3, GL_DOUBLE, GL_FALSE, 6 * sizeof(GLdouble), (void*)(3 * sizeof(GLdouble)));

    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    // Store the created buffers for later deletion
    data_traverse->gl_vaos.push_back(gl_vao);
    data_traverse->gl_vbos.push_back(gl_bo_vertex);
    data_traverse->gl_vbos.push_back(gl_bo_index);

    return { gl_vao, static_cast<GLsizei>(index_buffer.size()) };
}

Traversing the Model File

We start from the “leaf” of our operation: creating the OpenGL buffer from an A3DMeshData object. Now, our goal is to traverse the entire model file to the representation items where we call gl_make_buffers().

Our traversal has 3 main functions: traverse_representation_item(), traverse_part_definition(), and traverse_product_occurrence(). They are called whenever our traversal respectively encounters a representation item, a part definition, and a product occurrence.

All 4 functions take 4 arguments:

  • A handle to the traversed entity

  • The accumulated cascaded attributes, computed while traversing the parent entities

  • A model matrix, computed during traversal to establish the final model matrix of each object

  • A unique instance of TraverseData used to hold the results of our traversal.

Let’s start with the representation item first and go up to the model file.

Representation Item

The goal of traversing a representation item is to end up having a new SceneObject pushed into TraverseData::object.

void traverse_representation_item(A3DAsmProductOccurrence* const hnd_ri, A3DMiscCascadedAttributes* const hnd_attrs_part, const mat4x4 mat_transform_world, TraverseData* const data_traverse)
{
     // Code
}

Within a PRC, a representation item can be a single object or itself an aggregate of representation items. When this happens, the entity type of the representation item is set as kA3DTypeRiSet. In this case, we need to recursively traverse the list of representation items.

 3A3DEEntityType ri_type = kA3DTypeUnknown;
 4A3DEntityGetType(hnd_ri, &ri_type);
 5
 6if (ri_type == kA3DTypeRiSet) {
 7   A3DRiSetData data_ri_set;
 8   A3D_INITIALIZE_DATA(A3DRiSetData, data_ri_set);
 9   code = A3DRiSetGet(hnd_ri, &data_ri_set);
10   assert(code == A3D_SUCCESS);
11
12   for(A3DUns32 ri_i = 0 ; ri_i < data_ri_set.m_uiRepItemsSize ; ++ri_i) {
13       traverse_representation_item(data_ri_set.m_ppRepItems[ri_i], hnd_attrs_part, mat_transform_world, data_traverse);
14   }
15
16   A3DRiSetGet(0, &data_ri_set);
17} else
18     // ...

Now that we are sure we are dealing with a single representation item, we use A3DRiComputeMesh() to query a new A3DMeshData from our representation item. As seen in Getting Tessellation using A3DMeshData, a first call to our function can result in an invalid return code if the tessellation has not been computed before. In this case, we call A3DRiRepresentationItemComputeTessellation() before recomputing the mesh data:

18A3DMeshData data_mesh;
19A3D_INITIALIZE_DATA(A3DMeshData, data_mesh);
20code = A3DRiComputeMesh(hnd_ri, hnd_attrs_part, &data_mesh);
21if (code != A3D_SUCCESS)
22{
23    A3DRWParamsTessellationData data_params_tess;
24    A3D_INITIALIZE_DATA(A3DRWParamsTessellationData, data_params_tess);
25    data_params_tess.m_eTessellationLevelOfDetail = kA3DTessLODMedium;
26    A3DRiRepresentationItemComputeTessellation(hnd_ri, &data_params_tess);
27    code = A3DRiComputeMesh(hnd_ri, hnd_attrs_part, &data_mesh);
28}
29assert(code == A3D_SUCCESS);

Now that we have our instance of A3DMeshData, we can use gl_make_buffers() to generate the buffer object. However, we want to save some GPU memory here. In a PRC, a single representation item is often used many times (think about a model representing a bolt or a screw). We want to be sure we don’t generate buffer objects for representation items we already have.

Hence TraverseData::ri_to_gl. This associative container maps a representation item handle (hnd_ri) to a pair of values: the OpenGL vertex array object and the number of indices to render upon drawing the screen. Both values happen to be returned by our function gl_make_buffers().

traverse_representation_item() first searches in ri_to_gl if the representation item is already registered. If it’s the case, we use the mapped pair of values. If not, gl_make_buffers() is called, and its result is used to insert a new object.

31auto gl_iterator = data_traverse->ri_to_gl.find(hnd_ri);
32if(gl_iterator == data_traverse->ri_to_gl.end()) {
33    auto pair = gl_make_buffers(&data_mesh, data_traverse);
34    gl_iterator = data_traverse->ri_to_gl.insert({hnd_ri, pair}).first;
35}

After this code, we have:

  • A Vertex Array Object (gl_iterator->second.first)

  • A number of indices to draw (gl_iterator->second.second)

  • A model matrix for this instance of the VAO (mat_transform_world)

We can create a new scene object and push it into our collection (set in TraverseData):

37SceneObject object;
38mat4x4_dup(object.mat_transform_model, mat_transform_world);
39object.gl_vao = gl_iterator->second.first;
40object.gl_indices_count = gl_iterator->second.second;
41data_traverse->objects.push_back(object);

The function does not return anything, since the resulting scene object now lies in TraverseData::objects. The full function code is available here:

 1void traverse_representation_item(A3DRiRepresentationItem* const hnd_ri, A3DMiscCascadedAttributes* const hnd_attrs_part, const mat4x4 mat_transform_world, TraverseData* const data_traverse)
 2{
 3    A3DEEntityType ri_type = kA3DTypeUnknown;
 4    A3DEntityGetType(hnd_ri, &ri_type);
 5
 6    if (ri_type == kA3DTypeRiSet) {
 7        A3DRiSetData data_ri_set;
 8        A3D_INITIALIZE_DATA(A3DRiSetData, data_ri_set);
 9        code = A3DRiSetGet(hnd_ri, &data_ri_set);
10        assert(code == A3D_SUCCESS);
11
12        for(A3DUns32 ri_i = 0 ; ri_i < data_ri_set.m_uiRepItemsSize ; ++ri_i) {
13            traverse_representation_item(data_ri_set.m_ppRepItems[ri_i], hnd_attrs_part, mat_transform_world, data_traverse);
14        }
15
16        A3DRiSetGet(0, &data_ri_set);
17    } else {
18        A3DMeshData data_mesh;
19        A3D_INITIALIZE_DATA(A3DMeshData, data_mesh);
20        code = A3DRiComputeMesh(hnd_ri, hnd_attrs_part, &data_mesh);
21        if (code != A3D_SUCCESS)
22        {
23            A3DRWParamsTessellationData data_params_tess;
24            A3D_INITIALIZE_DATA(A3DRWParamsTessellationData, data_params_tess);
25            data_params_tess.m_eTessellationLevelOfDetail = kA3DTessLODMedium;
26            A3DRiRepresentationItemComputeTessellation(hnd_ri, &data_params_tess);
27            code = A3DRiComputeMesh(hnd_ri, hnd_attrs_part, &data_mesh);
28        }
29        assert(code == A3D_SUCCESS);
30
31        auto gl_iterator = data_traverse->ri_to_gl.find(hnd_ri);
32        if(gl_iterator == data_traverse->ri_to_gl.end()) {
33            auto pair = gl_make_buffers(&data_mesh, data_traverse);
34            gl_iterator = data_traverse->ri_to_gl.insert({hnd_ri, pair}).first;
35        }
36
37        SceneObject object;
38        mat4x4_dup(object.mat_transform_model, mat_transform_world);
39        object.gl_vao = gl_iterator->second.first;
40        object.gl_indices_count = gl_iterator->second.second;
41        data_traverse->objects.push_back(object);
42
43    }
44}

traverse_representation_item() is either called recursively by another traverse_representation_item() or upon visiting a part definition in traverse_part_definition().

Part Definition

Traversing the part definition is a pass-through function that does the following:

  • Updates the accumulated cascaded attributes (see Updating the Cascaded Attributes in the next function below).

  • Calls traverse_representation_item() for each representation item in the entity.

 1void traverse_part_definition(A3DAsmPartDefinition* const hnd_part, A3DMiscCascadedAttributes* const hnd_attrs_po, const mat4x4 mat_transform_world, TraverseData* const data_traverse)
 2{
 3    if(hnd_part == 0) {
 4        return;
 5    }
 6
 7    A3DMiscCascadedAttributes* hnd_attrs_part = 0;
 8    A3DMiscCascadedAttributesCreate(&hnd_attrs_part);
 9    A3DMiscCascadedAttributesPush(hnd_attrs_part, hnd_part, hnd_attrs_po);
10
11    A3DAsmPartDefinitionData data_part;
12    A3D_INITIALIZE_DATA(A3DAsmPartDefinitionData, data_part);
13    code = A3DAsmPartDefinitionGet(hnd_part, &data_part);
14    assert(code == A3D_SUCCESS);
15
16    for (A3DUns32 ri_i = 0 ; ri_i < data_part.m_uiRepItemsSize ; ++ri_i) {
17        traverse_representation_item(data_part.m_ppRepItems[ri_i], hnd_attrs_part, mat_transform_world, data_traverse);
18    }
19
20    A3DAsmPartDefinitionGet(0, &data_part);
21}

traverse_part_definition() is called whenever we find a new part definition from traverse_product_occurrence().

Product Occurrence

Going one level up in our PRC hierarchy, we find traverse_product_occurrence().

Updating the Cascaded Attributes

The first thing the function does is stack up the cascaded attributes of our current product occurrence onto the cascaded attributes stack. For this, we use A3DMiscCascadedAttributesCreate() to create a new instance of A3DMiscCascadedAttributes. Then we push it onto the stack with A3DMiscCascadedAttributesPush(). Finally, we need to attach the new cascaded attributes entity to our current product occurrence with A3DMiscCascadedAttributesEntityReferencePush(). This function is necessary to preserve the propagation of cascaded attributes upstream when the product occurrence is an assembly.

4A3DMiscCascadedAttributes* hnd_attrs_po = 0;
5A3DMiscCascadedAttributesCreate(&hnd_attrs_po);
6A3DMiscCascadedAttributesPush(hnd_attrs_po, hnd_po, hnd_attrs_parent);
7A3DMiscCascadedAttributesEntityReferencePush(hnd_attrs_po, hnd_po, 0);

Computing the Model Matrix

After fetching the product occurrence data with A3DAsmProductOccurrenceGet(), the next thing we do is compute the current model matrix. The computation is done by generating the 4x4 transform matrix of the current transformation and multiplying it by the current one passed as a parameter to our function:

16A3DMiscTransformation* hnd_po_transformation = get_product_occurrence_transformation(hnd_po);
17mat4x4 mat_transform_po_local, mat_transform_po_world;
18mat4x4_from_transformation(hnd_po_transformation, mat_transform_po_local);
19mat4x4_mul(mat_transform_po_world, mat_transform_world, mat_transform_po_local);

This code uses two custom functions that we also need to define:

First, retrieving the correct A3DMiscTransformation is not a direct operation. If the current product occurrence doesn’t have one and happens to be a prototype product occurrence, we want to retrieve the transformation entity from the prototype instead. This leads to the creation of get_product_occurrence_transformation() that returns the correct transformation entity given how our product occurrence is defined:

 1A3DMiscTransformation* get_product_occurrence_transformation(A3DAsmProductOccurrence* hnd_po)
 2{
 3    if(hnd_po == 0) {
 4        return 0;
 5    }
 6
 7    A3DAsmProductOccurrenceData data_po;
 8    A3D_INITIALIZE_DATA(A3DAsmProductOccurrenceData, data_po);
 9    code = A3DAsmProductOccurrenceGet(hnd_po, &data_po);
10    assert(code == A3D_SUCCESS);
11
12    A3DMiscTransformation* hnd_po_transformation = data_po.m_pLocation;
13    if (hnd_po_transformation == 0) {
14        A3DAsmProductOccurrence* hnd_po_reference = data_po.m_pPrototype ? data_po.m_pPrototype : data_po.m_pExternalData;
15        hnd_po_transformation = get_product_occurrence_transformation(hnd_po_reference);
16    }
17
18    A3DAsmProductOccurrenceGet(0, &data_po);
19    return hnd_po_transformation;
20}

Then, A3DMiscTransformation needs to be converted into a column-major model matrix that can be manipulated by our math and graphics libraries (linmath and OpenGL):

 1void mat4x4_from_transformation(const A3DMiscTransformation* hnd_transformation, mat4x4 mat_result)
 2{
 3    if (hnd_transformation == 0) {
 4        mat4x4_identity(mat_result);
 5    } else {
 6        A3DEEntityType entity_type = kA3DTypeUnknown;
 7        A3DEntityGetType(hnd_transformation, &entity_type);
 8        assert(entity_type == kA3DTypeMiscCartesianTransformation);
 9
10        A3DMiscCartesianTransformationData data;
11        A3D_INITIALIZE_DATA(A3DMiscCartesianTransformationData, data);
12        code = A3DMiscCartesianTransformationGet(hnd_transformation, &data);
13        assert(code == A3D_SUCCESS);
14
15
16        vec3 x_vector = {(float)data.m_sXVector.m_dX, (float)data.m_sXVector.m_dY, (float)data.m_sXVector.m_dZ};
17        vec3 y_vector = {(float)data.m_sYVector.m_dX, (float)data.m_sYVector.m_dY, (float)data.m_sYVector.m_dZ};
18        vec3 z_vector;
19        vec3_mul_cross(z_vector, x_vector, y_vector);
20
21        double mirror = ( data.m_ucBehaviour & kA3DTransformationMirror ) ? -1. : 1.;
22
23        mat_result[0][0] = data.m_sXVector.m_dX * data.m_sScale.m_dX;
24        mat_result[0][1] = data.m_sXVector.m_dY * data.m_sScale.m_dX;
25        mat_result[0][2] = data.m_sXVector.m_dZ * data.m_sScale.m_dX;
26        mat_result[0][3] = 0.;
27
28        mat_result[1][0] = data.m_sYVector.m_dX * data.m_sScale.m_dY;
29        mat_result[1][1] = data.m_sYVector.m_dY * data.m_sScale.m_dY;
30        mat_result[1][2] = data.m_sYVector.m_dZ * data.m_sScale.m_dY;
31        mat_result[1][3] = 0.;
32
33        mat_result[2][0] = mirror * z_vector[0] * data.m_sScale.m_dZ;
34        mat_result[2][1] = mirror * z_vector[1] * data.m_sScale.m_dZ;
35        mat_result[2][2] = mirror * z_vector[2] * data.m_sScale.m_dZ;
36        mat_result[2][3] = 0.;
37
38        mat_result[3][0] = data.m_sOrigin.m_dX;
39        mat_result[3][1] = data.m_sOrigin.m_dY;
40        mat_result[3][2] = data.m_sOrigin.m_dZ;
41        mat_result[3][3] = 1.;
42
43        A3DMiscCartesianTransformationGet(0, &data);
44    }
45}

Traversing the Part Definition

Back to our traverse_product_occurrence() function, we now have everything we need to call traverse_part_definition():

21A3DAsmPartDefinition* hnd_part = get_product_occurrence_part_definition(hnd_po);
22traverse_part_definition(hnd_part, hnd_attrs_po, mat_transform_po_world, data_traverse);

Similarly to A3DMiscTransformations, get_product_occurrence_part_definition() returns the correct A3DAsmPartDefinition for a given product occurrence. The code is the same as for get_product_occurrence_transformation():

 1A3DAsmPartDefinition* get_product_occurrence_part_definition(A3DAsmProductOccurrence* hnd_po)
 2{
 3    if(hnd_po == 0) {
 4        return 0;
 5    }
 6
 7    A3DAsmProductOccurrenceData data_po;
 8    A3D_INITIALIZE_DATA(A3DAsmProductOccurrenceData, data_po);
 9    code = A3DAsmProductOccurrenceGet(hnd_po, &data_po);
10    assert(code == A3D_SUCCESS);
11
12    A3DMiscTransformation* hnd_part = data_po.m_pPart;
13    if (hnd_part == 0) {
14        A3DAsmProductOccurrence* hnd_po_reference = data_po.m_pPrototype ? data_po.m_pPrototype : data_po.m_pExternalData;
15        hnd_part = get_product_occurrence_part_definition(hnd_po_reference);
16    }
17
18    A3DAsmProductOccurrenceGet(0, &data_po);
19    return hnd_part;
20}

Recursively Traverse the Product Occurrences

PRC is a recursive structure where each product occurrence contains a set of child product occurrences. Thus, when we are done with our current product occurrence, we want to call traverse_product_occurrence() again for each of its children:

24for (A3DUns32 po_i = 0 ; po_i < data_po.m_uiPOccurrencesSize ; ++po_i) {
25    traverse_product_occurrence(data_po.m_ppPOccurrences[po_i], hnd_attrs_po, mat_transform_po_world, data_traverse);
26}

The full code for traverse_product_occurrence() is available here:

 1void traverse_product_occurrence(A3DAsmProductOccurrence* const hnd_po, A3DMiscCascadedAttributes* const hnd_attrs_parent, const mat4x4 mat_transform_world, TraverseData* const data_traverse)
 2{
 3
 4    A3DMiscCascadedAttributes* hnd_attrs_po = 0;
 5    A3DMiscCascadedAttributesCreate(&hnd_attrs_po);
 6    A3DMiscCascadedAttributesPush(hnd_attrs_po, hnd_po, hnd_attrs_parent);
 7    A3DMiscCascadedAttributesEntityReferencePush(hnd_attrs_po, hnd_po, 0);
 8
 9
10    A3DAsmProductOccurrenceData data_po;
11    A3D_INITIALIZE_DATA(A3DAsmProductOccurrenceData, data_po);
12    code = A3DAsmProductOccurrenceGet(hnd_po, &data_po);
13    assert(code == A3D_SUCCESS);
14
15    // Accumulate the position
16    A3DMiscTransformation* hnd_po_transformation = get_product_occurrence_transformation(hnd_po);
17    mat4x4 mat_transform_po_local, mat_transform_po_world;
18    mat4x4_from_transformation(hnd_po_transformation, mat_transform_po_local);
19    mat4x4_mul(mat_transform_po_world, mat_transform_world, mat_transform_po_local);
20
21    A3DAsmPartDefinition* hnd_part = get_product_occurrence_part_definition(hnd_po);
22    traverse_part_definition(hnd_part, hnd_attrs_po, mat_transform_po_world, data_traverse);
23
24    for (A3DUns32 po_i = 0 ; po_i < data_po.m_uiPOccurrencesSize ; ++po_i) {
25        traverse_product_occurrence(data_po.m_ppPOccurrences[po_i], hnd_attrs_po, mat_transform_po_world, data_traverse);
26    }
27
28    A3DAsmProductOccurrenceGet(0, &data_po);
29}

traverse_product_occurrence() is thus called either from a parent call of traverse_product_occurrence() or from traverse_model_file().

Model File

traverse_model_file() is the entry point of our traversal operation.

The function initializes the first cascaded attribute as well as the transform matrix and calls traverse_product_occurrence() for each product occurrence in the given model file:

 1void traverse_model_file(A3DAsmModelFile* const hnd_modelfile, TraverseData* const data_traverse)
 2{
 3    A3DMiscCascadedAttributes* hnd_attrs_modelfile = 0;
 4    A3DMiscCascadedAttributesCreate(&hnd_attrs_modelfile);
 5
 6    A3DAsmModelFileData data_modelfile;
 7    A3D_INITIALIZE_DATA(A3DAsmModelFileData, data_modelfile);
 8    code = A3DAsmModelFileGet(hnd_modelfile, &data_modelfile);
 9    assert(code == A3D_SUCCESS);
10
11    mat4x4 mat_transform;
12    mat4x4_identity(mat_transform);
13
14    for (A3DUns32 po_i = 0 ; po_i < data_modelfile.m_uiPOccurrencesSize ; ++po_i) {
15        traverse_product_occurrence(data_modelfile.m_ppPOccurrences[po_i], hnd_attrs_modelfile, mat_transform, data_traverse);
16    }
17
18    A3DAsmModelFileGet(0, &data_modelfile);
19    A3DMiscCascadedAttributesDelete(hnd_attrs_modelfile);
20}

traverse_model_file() is directly called from main() once the model file has been loaded with load_model_file() and the OpenGL context is prepared.

The Main Loop

Our main loop function is a mix of GLFW and OpenGL code.

Before we run the loop, we need to retrieve the shader locations for our transform matrices. We have 3 matrices (the model, the view, and the projection), thus we have 3 variables:

3GLint p_location = glGetUniformLocation(program, "p");
4GLint v_location = glGetUniformLocation(program, "v");
5GLint m_location = glGetUniformLocation(program, "m");

Now, within the loop, we prepare the view and projection matrices. The projection matrix p never changes and corresponds to an orthographic view:

17mat4x4_ortho(p, -150.f, 150.f, -150.f, 150.f, -1000, 1000.0);

The view matrix will simulate a rotating camera around our model:

19mat4x4_identity(v);
20float radius = 150.f;
21float angle  = glfwGetTime();
22vec3 eye     = {cosf(angle) * radius, cosf(angle / 100.0) * radius, sinf(angle) * radius};
23vec3 center  = {0.0, 0.0, 0.0};
24vec3 up      = {0.0, 1.0, 0.0};
25mat4x4_look_at(v, eye, center, up);

Now, each object we created in traverse_representation_item() is drawn. The following loop goes through the array we previously populated in TraverseData. For each of the elements, we send the Vertex Array Object ID, the model matrix, as well as the number of indices the mesh is made of.

Eventually, we ask GLFW to swap our render buffer and enable window event listening:

27glUseProgram(program);
28
29for(const SceneObject* object = object_start ; object <= object_start + n_objects ; ++object) {
30    glUniformMatrix4fv(p_location, 1, GL_FALSE, (const GLfloat*) p);
31    glUniformMatrix4fv(v_location, 1, GL_FALSE, (const GLfloat*) v);
32    glUniformMatrix4fv(m_location, 1, GL_FALSE, (const GLfloat*) object->mat_transform_model);
33
34    glBindVertexArray(object->gl_vao);
35    glDrawElements(GL_TRIANGLES, object->gl_indices_count, GL_UNSIGNED_INT, 0);
36}
37
38glfwSwapBuffers(window);
39glfwPollEvents();

The full code for glfw_loop() is available here:

 1void glfw_loop(GLFWwindow* window, GLuint program, const SceneObject* object_start, size_t n_objects)
 2{
 3    GLint p_location = glGetUniformLocation(program, "P");
 4    GLint v_location = glGetUniformLocation(program, "V");
 5    GLint m_location = glGetUniformLocation(program, "M");
 6
 7    while (!glfwWindowShouldClose(window))
 8    {
 9        int width, height;
10        mat4x4 m, v, p;
11
12        glfwGetFramebufferSize(window, &width, &height);
13        glViewport(0, 0, width, height);
14
15        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
16
17        mat4x4_ortho(p, -150.f, 150.f, -150.f, 150.f, -1000, 1000.0);
18
19        mat4x4_identity(v);
20        float radius = 150.f;
21        float angle  = glfwGetTime();
22        vec3 eye     = {cosf(angle) * radius, cosf(angle / 100.0) * radius, sinf(angle) * radius};
23        vec3 center  = {0.0, 0.0, 0.0};
24        vec3 up      = {0.0, 1.0, 0.0};
25        mat4x4_look_at(v, eye, center, up);
26
27        glUseProgram(program);
28
29        for(const SceneObject* object = object_start ; object <= object_start + n_objects ; ++object) {
30            glUniformMatrix4fv(p_location, 1, GL_FALSE, (const GLfloat*) p);
31            glUniformMatrix4fv(v_location, 1, GL_FALSE, (const GLfloat*) v);
32            glUniformMatrix4fv(m_location, 1, GL_FALSE, (const GLfloat*) object->mat_transform_model);
33
34            glBindVertexArray(object->gl_vao);
35            glDrawElements(GL_TRIANGLES, object->gl_indices_count, GL_UNSIGNED_INT, 0);
36        }
37
38        glfwSwapBuffers(window);
39        glfwPollEvents();
40    }
41}

The Main Function

The main() function wraps up everything by calling all the preparation functions and the loop:

 1int main(int argc, char* argv[])
 2{
 3    GLFWwindow* window  = glfw_prepare();
 4    GLuint      program = gl_prepare_program();
 5
 6    A3DSDKLoadLibraryA(HE_BINARY_DIRECTORY);
 7    A3DLicPutUnifiedLicense(HOOPS_LICENSE);
 8    A3DDllInitialize(A3D_DLL_MAJORVERSION, A3D_DLL_MINORVERSION);
 9
10    A3DAsmModelFile* model_file = load_model_file(HE_DATA_DIRECTORY INPUT_FILE);
11
12    TraverseData     data_traverse;
13    traverse_model_file(model_file, &data_traverse);
14
15    A3DAsmModelFileDelete(model_file);
16    A3DDllTerminate();
17    A3DSDKUnloadLibrary();
18
19    printf("Starting Loop\n");
20    glfw_loop(window, program, data_traverse.objects.data(), data_traverse.objects.size());
21
22    // Clean up all resources
23    glDeleteProgram(program);
24    glDeleteVertexArrays(data_traverse.gl_vaos.size(), data_traverse.gl_vaos.data());
25    glDeleteBuffers(data_traverse.gl_vaos.size(), data_traverse.gl_vaos.data());
26
27    glfwDestroyWindow(window);
28    glfwTerminate();
29
30    return EXIT_SUCCESS;
31}

A complete version of main.cpp is avaiable in the project repository.

Note that in our example, once the buffer objects have been eventually created from a call to traverse_model_file(), HOOPS Exchange is not needed anymore and can be closed.

Compile and Run the Project

The project runs CMake and can be used with any supported platform.

From your environment, run CMake and select the project folder as the source directory. Then configure and generate the project.

Once generated, use your compiler’s environment to build and run the project.

For more information about using HOOPS Exchange with CMake, see Setting Up a CMake Project.