Understanding the mesh created by Qt3D
Asked Answered
O

1

6

I create a Qt3D mesh like this:

Qt3DCore::QEntity *newEntity = new Qt3DCore::QEntity();
Qt3DExtras::QConeMesh *mesh =new Qt3DExtras::QConeMesh();
mesh->setTopRadius(0.2);
mesh->setBottomRadius(1.0);
mesh->setLength(2.0);
for(int i = 0; i < mesh->geometry()->attributes().size(); ++i) {
    mesh->geometry()->attributes().at(i)->buffer()->setSyncData(true); // To have access to data
}
newEntity->addComponent(mesh);

The created mesh looks like this:

Cone Mesh Cone Mesh, another view


Later in the code, I try to export the above mesh in STL binary format. To do so, I extract the geometry and transformation components of the entity:

Qt3DCore::QComponent *compoMesh = nullptr; // place holder for mesh geometry of entity
Qt3DCore::QComponent *compoTran = nullptr; // place holder for mesh transformation of entity
QVector<Qt3DCore::QComponent *> compos = newEntity->components();
for(int i = 0; i < compos.size(); ++i) {
    if (qobject_cast<Qt3DRender::QGeometryRenderer *>(compos.at(i))) {
        compoMesh = compos.at(i); // mesh geometry component
    } else if (qobject_cast<Qt3DCore::QTransform *>(compos.at(i))) {
        compoTran = compos.at(i); // mesh transformation component
    }
}

Then I get the buffer data containing the vertex positions and normals:

Qt3DRender::QGeometryRenderer *mesh = qobject_cast<Qt3DRender::QGeometryRenderer *>(compoMesh);
Qt3DRender::QGeometry *geometry = mesh->geometry();
QVector<Qt3DRender::QAttribute *> atts = geometry->attributes();

Now, we focus on vertex positions attribute and vertex normals attribute. We get the byte offset and byte stride for each, also we check if both are using the same data buffer:

for(int i = 0; i < atts.size(); ++i) {
        if(atts.at(i)->name() == Qt3DRender::QAttribute::defaultPositionAttributeName()) {
            byteOffsetPos = atts.at(i)->byteOffset();
            byteStridePos = atts.at(i)->byteStride();
            bufferPtrPos = atts.at(i)->buffer();
        } else if(atts.at(i)->name() == Qt3DRender::QAttribute::defaultNormalAttributeName()) {
            byteOffsetNorm = atts.at(i)->byteOffset();
            byteStrideNorm = atts.at(i)->byteStride();
            bufferPtrNorm = atts.at(i)->buffer();
        }
    }
if(bufferPtrPos != bufferPtrNorm) {
    qDebug() << __func__ << "!!! Buffer pointer for position and normal are NOT the same";
    // Throw error here
    }

Then I use the byte offset and byte stride to extract triangles and write them to STL file. However the exported STL is NOT good:

The exported STL is NOT good


I use the same code for exporting STL of custom meshes which works fine. However when I use the same code to export the Qt3D ready-made meshes like QConeMesh, the exported STL is NOT acceptable. Can anybody give me a hint.


UPDATE

As noted by @vre I'm going to post the rest of the code for writing triangles to STL file. It's a large code, I try my best to keep it clear and concise:

For getting triangle positions and normals, I loop over attributes and get the VertexBuffer buffer which stores all the positions and normals:

// I loop over attributes to get access to VertexBuffer buffer
for(int i = 0; i < atts.size(); ++i) {
    Qt3DRender::QBuffer *buffer = atts.at(i)->buffer();
    QByteArray data = buffer->data();
    // We focus on VertexBuffer, NOT IndexBuffer!
    if( buffer->type() == Qt3DRender::QBuffer::VertexBuffer ) {
        // Number of triangles is number of vertices divided by 3:
        quint32 trianglesCount = atts.at(i)->count() / 3;

        // For each triangle, extract vertex positions and normals
        for(int j = 0; j < trianglesCount; ++j) {
            // Index for each triangle positions data
            // Each triangle has 3 vertices, hence 3 factor:
            // We already know byte-offset and byte-stride for positions
            int idxPos  = byteOffsetPos  + j * 3 * byteStridePos ;
            // Index for each triangle normals data
            // Each tirangle has 3 normals (right?), hence 3 factor:
            // We already know byte-offset and byte-stride for normals
            int idxNorm = byteOffsetNorm + j * 3 * byteStrideNorm;

            // Get x, y, z positions for 1st vertex
            // I have already checked that attribute base type is float by: `atts.at(i)->vertexBaseType();`
            QByteArray pos0x = data.mid(idxPos + 0 * sizeof(float), sizeof(float));
            QByteArray pos0y = data.mid(idxPos + 1 * sizeof(float), sizeof(float));
            QByteArray pos0z = data.mid(idxPos + 2 * sizeof(float), sizeof(float));

            // Get x, y z for 1st normal
            QByteArray norm0x= data.mid(idxNorm + 0 * sizeof(float), sizeof(float));
            QByteArray norm0y= data.mid(idxNorm + 1 * sizeof(float), sizeof(float));
            QByteArray norm0z= data.mid(idxNorm + 2 * sizeof(float), sizeof(float));

            // Get x, y, z positions for 2nd vertex
            QByteArray pos1x = data.mid(idxPos  + 1 * byteStridePos + 0 * sizeof(float), sizeof(float));
            QByteArray pos1y = data.mid(idxPos  + 1 * byteStridePos + 1 * sizeof(float), sizeof(float));
            QByteArray pos1z = data.mid(idxPos  + 1 * byteStridePos + 2 * sizeof(float), sizeof(float));

            // Get x, y, z for 2nd normal
            QByteArray norm1x= data.mid(idxNorm + 1 * byteStrideNorm + 0 * sizeof(float), sizeof(float));
            QByteArray norm1y= data.mid(idxNorm + 1 * byteStrideNorm + 1 * sizeof(float), sizeof(float));
            QByteArray norm1z= data.mid(idxNorm + 1 * byteStrideNorm + 2 * sizeof(float), sizeof(float));

            // Get x, y, z positions for 3rd vertex
            QByteArray pos2x = data.mid(idxPos  + 2 * byteStridePos + 0 * sizeof(float), sizeof(float));
            QByteArray pos2y = data.mid(idxPos  + 2 * byteStridePos + 1 * sizeof(float), sizeof(float));
            QByteArray pos2z = data.mid(idxPos  + 2 * byteStridePos + 2 * sizeof(float), sizeof(float));

            // Get x, y, z for 3rd normal
            QByteArray norm2x= data.mid(idxNorm + 2 * byteStrideNorm+ 0 * sizeof(float), sizeof(float));
            QByteArray norm2y= data.mid(idxNorm + 2 * byteStrideNorm+ 1 * sizeof(float), sizeof(float));
            QByteArray norm2z= data.mid(idxNorm + 2 * byteStrideNorm+ 2 * sizeof(float), sizeof(float));

            // Convert x, y, z byte arrays into floats
            float floatPos0x;
            if ( pos0x.size() >= sizeof(floatPos0x) ) {
                floatPos0x = *reinterpret_cast<const float *>( pos0x.data() );
            }
            float floatPos0y;
            if ( pos0y.size() >= sizeof(floatPos0y) ) {
                floatPos0y = *reinterpret_cast<const float *>( pos0y.data() );
            }
            float floatPos0z;
            if ( pos0z.size() >= sizeof(floatPos0z) ) {
                floatPos0z = *reinterpret_cast<const float *>( pos0z.data() );
            }

            // Do the rest of byte-array to float conversions:
            // norm0x=>floatNorm0x, norm0y=>floatNorm0y, norm0z=>floatNorm0z
            // pos1x=>floatPos1x, pos1y=>floatPos1y, pos1z=>floatPos1z
            // norm1x=>floatNorm1x, norm1y=>floatNorm1y, norm1z=>floatNorm1z
            // pos2x=>floatPos2x, pos2y=>floatPos2y, pos2z=>floatPos2z
            // norm2x=>floatNorm2x, norm2y=>floatNorm2y, norm2z=>floatNorm2z

            // Compose positions matrix before applying transformations
            // I'm going to use `QMatrix4x4` but I have 3 vertices of 3x1
            // Therefore I have to fill out `QMatrix4x4` with zeros and ones
            // Please see this question and its answer: https://mcmap.net/q/1777626/-how-to-apply-transformations-to-mesh-geometry/3405291
            QMatrix4x4 floatPos4x4 = QMatrix4x4(
                floatPos0x, floatPos1x, floatPos2x, 0,
                floatPos0y, floatPos1y, floatPos2y, 0,
                floatPos0z, floatPos1z, floatPos2z, 0,
                1         , 1         , 1         , 0
            );

            // Apply transformations to positions:
            // We already have transformations component `compoTran` from previous code:
            Qt3DCore::QTransform *tran = qobject_cast<Qt3DCore::QTransform *>(compoTran);
            QMatrix4x4 newFloatPos4x4 = tran->matrix() * floatPos4x4;

            // Get new positions after applying transformations:
            float newFloatPos0x = newFloatPos4x4(0,0);
            float newFloatPos0y = newFloatPos4x4(1,0);
            float newFloatPos0z = newFloatPos4x4(2,0);

            float newFloatPos1x = newFloatPos4x4(0,1);
            float newFloatPos1y = newFloatPos4x4(1,1);
            float newFloatPos1z = newFloatPos4x4(2,1);

            float newFloatPos2x = newFloatPos4x4(0,2);
            float newFloatPos2y = newFloatPos4x4(1,2);
            float newFloatPos2z = newFloatPos4x4(2,2);

            // Convert all the floats (after applying transformations) back to byte array:
            QByteArray newPos0x( reinterpret_cast<const char *>( &newFloatPos0x ), sizeof( newFloatPos0x ) );
            QByteArray newPos0y( reinterpret_cast<const char *>( &newFloatPos0y ), sizeof( newFloatPos0y ) );
            QByteArray newPos0z( reinterpret_cast<const char *>( &newFloatPos0z ), sizeof( newFloatPos0z ) );

            QByteArray newPos1x( reinterpret_cast<const char *>( &newFloatPos1x ), sizeof( newFloatPos1x ) );
            QByteArray newPos1y( reinterpret_cast<const char *>( &newFloatPos1y ), sizeof( newFloatPos1y ) );
            QByteArray newPos1z( reinterpret_cast<const char *>( &newFloatPos1z ), sizeof( newFloatPos1z ) );

            QByteArray newPos2x( reinterpret_cast<const char *>( &newFloatPos2x ), sizeof( newFloatPos2x ) );
            QByteArray newPos2y( reinterpret_cast<const char *>( &newFloatPos2y ), sizeof( newFloatPos2y ) );
            QByteArray newPos2z( reinterpret_cast<const char *>( &newFloatPos2z ), sizeof( newFloatPos2z ) );

            // Log triangle vertex positions and normals (float numbers)
            // A sample log is posted on this question on StackOverflow
            qDebug() << __func__ << " pos 0: x " << newFloatPos0x << " y " << newFloatPos0y << " z " << newFloatPos0z;
            qDebug() << __func__ << " pos 1: x " << newFloatPos1x << " y " << newFloatPos1y << " z " << newFloatPos1z;
            qDebug() << __func__ << " pos 2: x " << newFloatPos2x << " y " << newFloatPos2y << " z " << newFloatPos2z;

            qDebug() << __func__ << " norm 0: x " << floatNorm0x << " y " << floatNorm0y << " z " << floatNorm0z;
            qDebug() << __func__ << " norm 1: x " << floatNorm1x << " y " << floatNorm1y << " z " << floatNorm1z;
            qDebug() << __func__ << " norm 2: x " << floatNorm2x << " y " << floatNorm2y << " z " << floatNorm2z;

            // Write the triangle to STL file
            // Note that STL file needs a header which is written in another section of code
            // Note that STL file needs total number of triangles which is written in another section of code
            // Note that STL file needs only one normal vector for each triangle, but here we have 3 normals (for 3 vertices), therefore I'm writing only the 1st normal to STL (is it OK?!)
            // `baStl` is a byte-array containing all the STL data
            // `baStl` byte-array is written to a file in another section of the code
            QBuffer tempBuffer(&baStl);
            tempBuffer.open(QIODevice::Append);
            tempBuffer.write( norm0x   ); // vertex 0 Normal vector
            tempBuffer.write( norm0y   );
            tempBuffer.write( norm0z   );
            tempBuffer.write( newPos0x ); // New vertex 0 position
            tempBuffer.write( newPos0y );
            tempBuffer.write( newPos0z );
            tempBuffer.write( newPos1x ); // New vertex 1 position
            tempBuffer.write( newPos1y );
            tempBuffer.write( newPos1z );
            tempBuffer.write( newPos2x ); // New vertex 2 position
            tempBuffer.write( newPos2y );
            tempBuffer.write( newPos2z );
            tempBuffer.write("aa"); // Attribute byte count: UINT16: 2 bytes: content doesn't matter, just write 2 bytes
            tempBuffer.close();
        }
    }
}

The above code works perfect for custom meshes. I mean when I import a STL file into my Qt3D application and then export it again as STL, the exported STL is good. The problem is: when creating Qt3D ready-made meshes like QConeMesh, the exported STL is screwed up, I mean the overall geometry is OK, but the triangles are messed up as shown in the above image.

My code logs following values when trying to export a QConeMesh. As can be seen, normal vectors have unit size which shows they are actually normals:

...
exportStlUtil  pos 0: x  -10.6902  y  -7.55854  z  4.76837e-07
exportStlUtil  pos 1: x  -12.8579  y  -4.31431  z  2.98023e-07
exportStlUtil  pos 2: x  -13.6191  y  -0.487476  z  5.96046e-08
exportStlUtil  norm 0: x  -0.707107  y  0  z  0.707107
exportStlUtil  norm 1: x  -0.92388  y  0  z  0.382683
exportStlUtil  norm 2: x  -1  y  0  z  -8.74228e-08
exportStlUtil  pos 0: x  -12.8579  y  3.33936  z  -1.19209e-07
exportStlUtil  pos 1: x  -10.6902  y  6.58359  z  -3.57628e-07
exportStlUtil  pos 2: x  -7.44594  y  8.75132  z  -4.76837e-07
exportStlUtil  norm 0: x  -0.92388  y  0  z  -0.382683
exportStlUtil  norm 1: x  -0.707107  y  0  z  -0.707107
exportStlUtil  norm 2: x  -0.382683  y  0  z  -0.92388
exportStlUtil  pos 0: x  -3.61911  y  9.51252  z  -4.76837e-07
exportStlUtil  pos 1: x  0.207723  y  8.75132  z  -4.76837e-07
exportStlUtil  pos 2: x  3.45196  y  6.58359  z  -3.57628e-07
exportStlUtil  norm 0: x  1.19249e-08  y  0  z  -1
exportStlUtil  norm 1: x  0.382684  y  0  z  -0.923879
exportStlUtil  norm 2: x  0.707107  y  0  z  -0.707107
exportStlUtil  pos 0: x  5.61968  y  3.33936  z  -1.19209e-07
exportStlUtil  pos 1: x  6.38089  y  -0.487479  z  5.96046e-08
exportStlUtil  pos 2: x  6.38089  y  -0.487477  z  0.133333
exportStlUtil  norm 0: x  0.92388  y  0  z  -0.382683
exportStlUtil  norm 1: x  1  y  0  z  1.74846e-07
exportStlUtil  norm 2: x  1  y  0  z  0
exportStlUtil  pos 0: x  5.61968  y  -4.31431  z  0.133334
exportStlUtil  pos 1: x  3.45195  y  -7.55854  z  0.133334
exportStlUtil  pos 2: x  0.207721  y  -9.72627  z  0.133334
exportStlUtil  norm 0: x  0.92388  y  0  z  0.382683
exportStlUtil  norm 1: x  0.707107  y  0  z  0.707107
exportStlUtil  norm 2: x  0.382683  y  0  z  0.92388
...
Obaza answered 27/8, 2018 at 12:52 Comment(7)
Can you show how you calculate the offset in the vertexBuffer using byteStride and byteOffset? As the default geometries of Qt3D use a combined vertex, normal and texture coordinate buffer I suppose the offset calculation is erroneous.Ting
@Ting thanks. I don't have access to the computer now, I'll post the code tomorrow ☺Obaza
@Ting I posted my code for getting triangle data out of VertexBuffer. I tried my best to make the posted code clear and concise. I appreciate if you take a look. Thanks :)Obaza
I supposed you did it that way, but you need to use the entries of the indexBuffer as this is the structure that holds the indices of the triangle vertices in the vertexBuffer. Instead of using a for loop over the vertexBuffer you need to use a for loop over the indexBuffer.Ting
@Ting Wow! I thought every 3 consecutive vertices on VertexBuffer belong to the same triangle! So, that's not the case, right?Obaza
No! The indexBuffer contains the indices to the vertexBuffer that form a triangle. Three consecutive entries form a triangle. See the source code of Qt3D (..\Qt\5.11.0\Src\qt3d\src\extras\geometries\qconegeometry.cpp) to see how the indexBuffer and the vertexBuffer for a cone are generated.Ting
@Ting Can you post your comments as answer, so that this question can be closes as answered. Thanks.Obaza
T
6

Reformulating my comments as an answer:

Qt3D default geometry consists mostly of at least two buffers, the vertexBuffer containing vertices, texture coordinates, as well as normals and the indexBuffer containing the indices that form triangles or triangleStrips. To access a vertex or a normal in the vertexBuffer you first look up a sequence of three consecutive indices from the indexBuffer and calculate the offset in the vertexBuffer taking vertexSize, byteStride and byteOffset into consideration.

To access the vertexCoord posx in a vertexBuffer of layout [vertexCoords, textureCoords, normalCoords] (the GeometryRenderer is of primitiveType Triangles) the calculation of the vertexBufferIndex would then be:

vertexBufPtr + indexBuffer(i) * byteStride + byteOffsetPos

and for the first normal coordinate

vertexBufPtr + indexBuffer(i) * byteStride + byteOffsetNormal.

byteStride equals to 8 * sizeof(float), byteOffsetPos equals to 0, and byteOffsetNormal to 5 * sizeof(float).

Ting answered 28/8, 2018 at 19:3 Comment(2)
How can I get the size of an index in the indexBuffer with the intent to decide which integer type it is? QBuffer::vertexSize() of indexBuffer returns 1. But this seems to be wrong because QBuffer::count() returns more than 255. Thanks.Impedance
When one consider QBuffer::count() and QByteArray::size() I guess that the index size is 2 (bytes).Impedance

© 2022 - 2024 — McMap. All rights reserved.