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:
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:
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.
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://stackoverflow.com/q/51979168/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 ...
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With