4. Write a CAD Viewer

Welcome to the Mesh Viewer Sample! If you’ve already set up your environment following environment Setup our guide and learned how to traverse a model file, you’re ready for a more hands-on experience.

This tutorial breaks down the Viewer Sample in 3 main focus points:

Prerequisites

Before you start this tutorial, make sure your environment is correctly configured. Refer to the Environment Setup Guide for detailed instructions.

To begin development, ensure you have the following:

  1. CMake: If not installed, download and install it from here.

    ../_images/tuto_viewer_cmake.png
  2. Source Code: The complete source code for this project is available on GitHub. Download and unzip it to your preferred location. Once you have CMake and the source code:

    • Run CMake.

    • Set the source directory to the path of the extracted folder.

    • Specify the binary directory path for project file generation.

    • Click the Generate button.

    Your project is now configured and ready for compilation and execution.

After successfully building and compiling the project, run the generated application. You’ll witness a rotating model displayed in a window:

../_images/basic_viewing.png

Getting Mesh Information

At the end of our model file traversal, we define the he_mesh_data_to_rendering() function, which takes a single parameter: mesh_data, an instance of A3DMeshData.

This structure provides a complete description of a triangle mesh, including:

  • A list of 3D coordinates the mesh is made of,

  • A list of 3D normals, and

  • A list of ordered indices, describing the topology of the 3D model.

The data structure also simplifies access to materials and style information. For simplicity, this tutorial focuses solely on mesh geometry.

Getting the 3D Coordinates

Each 3D coordinate, called a vertex, is represented as a triplet of three real values: X, Y, and Z. The m_pdCoords field is an array of these values in order:

{
  X, Y, Z, // Vertex #0
  X, Y, Z, // Vertex #1
  ...
  X, Y, Z, // Vertex #N-1
}

The total number of coordinates in m_pdCoords is three times the number of vertices, stored in m_uiCoordSize.

Our he_mesh_data_to_rendering() function uses these values and sends them into an instance of std::vector:

// ...

std::vector<GLdouble> vertex_buffer(
    mesh_data->m_pdCoords,                           // First vertex X
    mesh_data->m_pdCoords + mesh_data->m_uiCoordSize // Last vertex Z
);

// ...

Getting the Normal Coordinates

Normal coordinates follow the same structure as vertex coordinates. The data is stored in m_pdNormals, with the size set in m_uiNormalSize.

Here too, we instantiate normal_buffer, an std::vector that contains this information:

// ...

std::vector<GLdouble> normal_buffer(
    mesh_data->m_pdNormals,                            // First normal X
    mesh_data->m_pdNormals + mesh_data->m_uiNormalSize // Last normal Z
);

// ...

Getting the Topology

A mesh described in A3DMeshData is represented using ordered triangles.

Each triangle consists of three vertex/normal pairs, represented as indices taken simultaneously from the vertex and normal arrays. For example, if a triangle is represented with the 4, 10, and 7 triplet in order, it means that the first vertex of the triangle is represented by the values at index 4 of the vertex array and the normal array and so on.

A3DMeshData does not directly provide the total number of indices. Instead, triangles are grouped into faces, and the structure provides the number of triangles (and thus indices) per face.

Our he_mesh_data_to_rendering() function computes the total number of indices as n_indices using a for-loop:

size_t n_indices = 0;
for (A3DUns32 face_i = 0; face_i < mesh_data->m_uiFaceSize; ++face_i) {
    // Each face contributes 3 times the number of triangles to the total indices
    n_indices += 3 * mesh_data->m_puiTriangleCountPerFace[face_i];
}

The list of all indices is available in m_puiVertexIndicesPerFace, which we can also copy to an std::vector:

std::vector<GLuint> index_buffer(mesh_data->m_puiVertexIndicesPerFace, mesh_data->m_puiVertexIndicesPerFace + n_indices);

The Full Function

Now we have the list of vertices, normals, and indices as std::vector arrays that we send to rendering_to_gpu(), which is responsible for sending the data to the graphics API. he_mesh_data_to_rendering() returns two pieces of data that we will use later on: an ID generated by the graphics API that points to the drawable object and the number of indices the mesh is made of.


The full function looks like this:

std::pair<GLuint, GLsizei> he_mesh_data_to_rendering(A3DMeshData* const mesh_data)
{
   // Extract vertex coordinates
   std::vector<GLdouble> vertex_buffer(mesh_data->m_pdCoords, mesh_data->m_pdCoords + mesh_data->m_uiCoordSize);

   // Extract normal coordinates
   std::vector<GLdouble> normal_buffer(mesh_data->m_pdNormals, mesh_data->m_pdNormals + mesh_data->m_uiNormalSize);

   // Count the total number of indices
   size_t n_indices = 0;
   for (A3DUns32 face_i = 0; face_i < mesh_data->m_uiFaceSize; ++face_i) {
      n_indices += 3 * mesh_data->m_puiTriangleCountPerFace[face_i];
   }

   // Extract indices
   std::vector<GLuint> index_buffer(mesh_data->m_puiVertexIndicesPerFace, mesh_data->m_puiVertexIndicesPerFace + n_indices);

   // Send data to the graphics API and get a renderable ID
   GLuint renderable_id = rendering_to_gpu(index_buffer, vertex_buffer, normal_buffer);

   // Return the renderable ID and the number of indices
   return {renderable_id, (GLsizei)index_buffer.size()};
}

Now that we know how to read the information of A3DMeshData, let’s see how to traverse our model tree and when he_mesh_data_to_rendering() is called.

Traversing the Model Tree

HOOPS Exchange provides an easy way to traverse an entire model file through abstraction.

Using the A3DTreeCompute() function, you can obtain a tree-like structure of your model file, where each entity is abstracted away as an A3DTreeNode:

A3DTree*         hnd_tree = 0;

A3DStatus code = A3DTreeCompute(model_file, &hnd_tree, 0);
if (code == A3D_SUCCESS) {
    // Get the root tree node:
    A3DTreeNode* hnd_root_node = 0;
    A3DTreeGetRootNode(hnd_tree, &hnd_root_node);

    // Use model file as a tree structure...

    A3DTreeCompute(0, &hnd_tree, 0);
}

From there, a list of utility functions are provided in order to query node information, such as its CAD name or its child nodes.

In our case, we will be using a few of them to traverse the tree down to any renderable entity:

Let’s use these 4 functions to recursively traverse our entire model file, starting from the root node:

void he_traverse_tree(A3DTree* const hnd_tree, A3DTreeNode* const hnd_node, TraverseData* const data_traverse)
{
    // STEP 1: Getting Node Geometry.

    // STEP 2: Send Mesh Data to Graphics API.

    // STEP 3: Compute and Store Transform Matrix.

    // STEP 4: Visit Child Nodes.
}

TraverseData is what we call an accumulator. It stores information we computed during traversal, either for the sake of the traversal itself or in order to perform the eventual draw calls to the screen.

Getting Node Geometry

Computing a mesh from a node is done by calling A3DTreeNodeGetGeometry(). The function returns a valid A3DMeshData instance that must be released from memory when not used anymore:

void he_traverse_tree(A3DTree* const hnd_tree, A3DTreeNode* const hnd_node, TraverseData* const data_traverse)
{
    // STEP 1: Getting Node Geometry:
    A3DMeshData mesh_data;
    A3D_INITIALIZE_DATA(A3DMeshData, mesh_data);
    A3DStatus code = A3DTreeNodeGetGeometry(hnd_tree, hnd_node, A3D_TRUE, &mesh_data, 0);

    if(code == A3D_SUCCESS) {

        // STEP 2: Send Mesh Data to Graphics API...

        A3DTreeNodeGetGeometry(0, 0, A3D_TRUE, &mesh_data, 0);

        // STEP 3: Compute and Store Transform Matrix...

    }

    // STEP 4: Visit Child Nodes...
}

Send Mesh Data to Graphics API

To send our A3DMeshData to the graphics API, we use our he_mesh_data_to_rendering() function.

In order to save memory and compute time, data_traverse contains a cache dictionary (an std::unordered_map) that associated the underlying A3DEntity of a node to the graphics identity returned by he_mesh_data_to_rendering(). Thus, we only generate a new GPU object when we encounter a new renderable A3DEntity. Otherwise, we use the already generated one:

void he_traverse_tree(A3DTree* const hnd_tree, A3DTreeNode* const hnd_node, TraverseData* const data_traverse)
{
    // STEP 1: Getting Node Geometry...

    if(code == A3D_SUCCESS) {

        // STEP 2: Send Mesh Data to Graphics API:
        A3DEntity* hnd_ri = 0;
        A3DTreeNodeGetEntity(hnd_node, &hnd_ri);
        auto gl_iterator = data_traverse->ri_to_gl.find(hnd_ri);
        if(gl_iterator == data_traverse->ri_to_gl.end()) {
            auto pair = he_mesh_data_to_rendering(&mesh_data);
            gl_iterator = data_traverse->ri_to_gl.insert({hnd_ri, pair}).first;
        }

        // STEP 3: Compute and Store Transform Matrix...

    }

    // STEP 4: Visit Child Nodes...
}

Compute and Store Transform Matrix

We can identify each renderable object of our scene with 3 sets of data:

  • A handle to the GPU-side mesh,

  • the number of indices a mesh is made of, and

  • a set of transforms that place the object in the 3D space.

We store them in instances of SceneObject. gl_iterator comes from the previous section of our function.

SceneObject object;

// Compute the Transform Matrix...

object.gl_vao = gl_iterator->second.first;
object.gl_indices_count = gl_iterator->second.second;

// Push the instance into data_traverse:
data_traverse->objects.push_back(object);

HOOPS Exchange associates each entity with an A3DMiscTransfomation entity that contains its positioning in 3D space. We can get a node’s transformation with A3DTreeNodeGetNetTransformation().

Once obtained, we use he_transformation_to_mat4x4() to convert it to column-major 4x4 transform matrix that is compatible with our graphics API:

// ...

A3DMiscTransformation* hnd_net_transform = 0;
A3DTreeNodeGetNetTransformation(hnd_node, &hnd_net_transform);

he_transformation_to_mat4x4(hnd_net_transform, object.mat_transform_model);

// ...

All put together:

void he_traverse_tree(A3DTree* const hnd_tree, A3DTreeNode* const hnd_node, TraverseData* const data_traverse)
{
    // STEP 1: Getting Node Geometry...

    if(code == A3D_SUCCESS) {

        // STEP 2: Send Mesh Data to Graphics API...

        // STEP 3: Compute and Store Transform Matrix:
        A3DMiscTransformation* hnd_net_transform = 0;
        A3DTreeNodeGetNetTransformation(hnd_node, &hnd_net_transform);

        SceneObject object;
        he_transformation_to_mat4x4(hnd_net_transform, object.mat_transform_model);
        object.gl_vao = gl_iterator->second.first;
        object.gl_indices_count = gl_iterator->second.second;
        data_traverse->objects.push_back(object);

    }

    // STEP 4: Visit Child Nodes...
}

Visit Child Nodes

Once the tree node has been entirely visited, we can recursively call he_traverse_tree() on the child nodes. To do this, we first call A3DTreeNodeGetChildren() query them as an array. The function must be called again to release memory after use.

void he_traverse_tree(A3DTree* const hnd_tree, A3DTreeNode* const hnd_node, TraverseData* const data_traverse)
{
    // STEP 1: Getting Node Geometry...

    if(code == A3D_SUCCESS) {

        // STEP 2: Send Mesh Data to Graphics API...

        // STEP 3: Compute and Store Transform Matrix...

    }

    // STEP 4: Visit Child Nodes:
    A3DUns32 n_children        = 0;
    A3DTreeNode** hnd_children = 0;

    A3DTreeNodeGetChildren(hnd_tree, hnd_node, &n_children, &hnd_children);
    for (size_t c = 0 ; c < n_children ; ++c) {
        he_traverse_tree(hnd_tree, hnd_children[c], data_traverse);
    }
    A3DTreeNodeGetChildren(0, 0, &n_children, &hnd_children);
}

Wrapping Up

The entire definition for he_traverse_tree() looks like this:

void he_traverse_tree(A3DTree* const hnd_tree, A3DTreeNode* const hnd_node, TraverseData* const data_traverse)
{
    // STEP 1: Getting Node Geometry...
    A3DMeshData mesh_data;
    A3D_INITIALIZE_DATA(A3DMeshData, mesh_data);
    A3DStatus code = A3DTreeNodeGetGeometry(hnd_tree, hnd_node, A3D_TRUE, &mesh_data, 0);

    if(code == A3D_SUCCESS) {

        // STEP 2: Send Mesh Data to Graphics API:
        A3DEntity* hnd_ri = 0;
        A3DTreeNodeGetEntity(hnd_node, &hnd_ri);
        auto gl_iterator = data_traverse->ri_to_gl.find(hnd_ri);
        if(gl_iterator == data_traverse->ri_to_gl.end()) {
            auto pair = he_mesh_data_to_rendering(&mesh_data);
            gl_iterator = data_traverse->ri_to_gl.insert({hnd_ri, pair}).first;
        }

        // Release the mesh data memory:
        A3DTreeNodeGetGeometry(0, 0, A3D_TRUE, &mesh_data, 0);

        // STEP 3: Compute and Store Transform Matrix:
        A3DMiscTransformation* hnd_net_transform = 0;
        A3DTreeNodeGetNetTransformation(hnd_node, &hnd_net_transform);

        // Store the drawable object:
        SceneObject object;
        he_transformation_to_mat4x4(hnd_net_transform, object.mat_transform_model);
        object.gl_vao = gl_iterator->second.first;
        object.gl_indices_count = gl_iterator->second.second;
        data_traverse->objects.push_back(object);
    }

    // STEP 4: Visit Child Nodes:
    A3DUns32 n_children        = 0;
    A3DTreeNode** hnd_children = 0;

    code = A3DTreeNodeGetChildren(hnd_tree, hnd_node, &n_children, &hnd_children);
    assert(code == A3D_SUCCESS);
    for (size_t c = 0 ; c < n_children ; ++c) {
        he_traverse_tree(hnd_tree, hnd_children[c], data_traverse);
    }
    A3DTreeNodeGetChildren(0, 0, &n_children, &hnd_children);
}

Loading a CAD File

Now we learned how to traverse a model file and send its mesh data to the graphics API, we’re ready to load a CAD file and call our traversal function.

In the main() function, the model file is loaded through the A3DSDKHOOPSExchangeLoader helper class. At the end of the call, model_file contains a valid handle to your loaded CAD file.

Connecting things together, we then instantiate the A3DTree for our model file and call our traversal function on its root node:

int main(int argc, char* argv[])
{
     // Initialize functions...


     // Load the Model File:

     A3DImport he_import(HE_DATA_DIRECTORY INPUT_FILE);
     he_loader.Import(he_import);
     A3DAsmModelFile* model_file = he_loader.m_psModelFile;

     TraverseData     data_traverse;
     A3DTree*         hnd_tree = 0;


     // Traverse is as a tree:

     A3DTreeCompute(model_file, &hnd_tree, 0);

     A3DTreeNode* hnd_root_node = 0;
     A3DTreeGetRootNode(hnd_tree, &hnd_root_node);

     he_traverse_tree(hnd_tree, hnd_root_node, &data_traverse);

     A3DTreeCompute(0, &hnd_tree, 0);


     // Cleanup functions...
}