QGIS API Documentation 4.1.0-Master (659fe69c07c)
Loading...
Searching...
No Matches
qgsobj3dutils.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsobj3dutils.cpp
3 --------------------------------------
4 Date : April 2026
5 Copyright : (C) 2026 by Dominik Cindrić
6 Email : viper dot miniq at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#define TINYOBJLOADER_IMPLEMENTATION
17#define TINYOBJLOADER_USE_MAPBOX_EARCUT
18#include "qgsobj3dutils.h"
19
20#include <tiny_obj_loader.h>
21#include <unordered_map>
22
23#include "qgs3dutils.h"
24#include "qgsimagetexture.h"
25#include "qgslogger.h"
27#include "qgsphongmaterial.h"
29
30#include <QDir>
31#include <QFileInfo>
32#include <QImage>
33#include <QString>
34#include <Qt3DCore/QAttribute>
35#include <Qt3DCore/QBuffer>
36#include <Qt3DCore/QGeometry>
37#include <Qt3DRender/QTexture>
38
39using namespace Qt::StringLiterals;
40
42
43std::vector<QgsMeshNodeData> QgsObj3DUtils::buildObjGeometries( const QString &filePath, const QgsMaterialContext &materialContext )
44{
45 tinyobj::ObjReaderConfig config;
46 config.triangulate = true;
47 config.triangulation_method = "earcut";
48 config.mtl_search_path = QFileInfo( filePath ).absolutePath().toStdString();
49
50 tinyobj::ObjReader reader;
51 if ( !reader.ParseFromFile( filePath.toStdString(), config ) )
52 {
53 if ( !reader.Error().empty() )
54 QgsDebugMsgLevel( u"tinyobj error loading '%1': %2"_s.arg( filePath, QString::fromStdString( reader.Error() ) ), 1 );
55 return {};
56 }
57 if ( !reader.Warning().empty() )
58 QgsDebugMsgLevel( u"tinyobj warning loading '%1': %2"_s.arg( filePath, QString::fromStdString( reader.Warning() ) ), 2 );
59
60 const tinyobj::attrib_t &attrib = reader.GetAttrib();
61 const std::vector<tinyobj::shape_t> &shapes = reader.GetShapes();
62 const std::vector<tinyobj::material_t> &materials = reader.GetMaterials();
63
64 const QString objDir = QFileInfo( filePath ).absolutePath();
65
66 const bool hasNormals = !attrib.normals.empty();
67 const bool hasTexCoords = !attrib.texcoords.empty();
68
69 const size_t floatsPerVertex = 3 + ( hasNormals ? 3 : 0 ) + ( hasTexCoords ? 2 : 0 );
70
71 std::unordered_map<int, std::vector<float>> matDataMap;
72
73 // see https://github.com/tinyobjloader/tinyobjloader
74 for ( size_t shapeIdx = 0; shapeIdx < shapes.size(); shapeIdx++ )
75 {
76 size_t index_offset = 0;
77 for ( size_t faceIdx = 0; faceIdx < shapes[shapeIdx].mesh.num_face_vertices.size(); faceIdx++ )
78 {
79 const size_t faceVertexCount = size_t( shapes[shapeIdx].mesh.num_face_vertices[faceIdx] );
80 const int matId = shapes[shapeIdx].mesh.material_ids[faceIdx];
81 std::vector<float> &data = matDataMap[matId];
82
83 data.reserve( data.size() + faceVertexCount * floatsPerVertex );
84
85 for ( size_t v = 0; v < faceVertexCount; v++ )
86 {
87 const tinyobj::index_t idx = shapes[shapeIdx].mesh.indices[index_offset + v];
88
89 data.push_back( attrib.vertices[3 * size_t( idx.vertex_index ) + 0] );
90 data.push_back( attrib.vertices[3 * size_t( idx.vertex_index ) + 1] );
91 data.push_back( attrib.vertices[3 * size_t( idx.vertex_index ) + 2] );
92
93 if ( hasNormals )
94 {
95 if ( idx.normal_index >= 0 )
96 {
97 data.push_back( attrib.normals[3 * size_t( idx.normal_index ) + 0] );
98 data.push_back( attrib.normals[3 * size_t( idx.normal_index ) + 1] );
99 data.push_back( attrib.normals[3 * size_t( idx.normal_index ) + 2] );
100 }
101 else
102 {
103 data.push_back( 0.0f );
104 data.push_back( 0.0f );
105 data.push_back( 0.0f );
106 }
107 }
108
109 if ( hasTexCoords )
110 {
111 if ( idx.texcoord_index >= 0 )
112 {
113 data.push_back( attrib.texcoords[2 * size_t( idx.texcoord_index ) + 0] );
114 data.push_back( 1.0f - attrib.texcoords[2 * size_t( idx.texcoord_index ) + 1] ); // flip v
115 }
116 else
117 {
118 data.push_back( 0.0f );
119 data.push_back( 0.0f );
120 }
121 }
122 }
123 index_offset += faceVertexCount;
124 }
125 }
126
127 std::vector<QgsMeshNodeData> result;
128 result.reserve( matDataMap.size() );
129
130 for ( auto &[matId, vertices] : matDataMap )
131 {
132 if ( vertices.empty() )
133 continue;
134
135 Qt3DCore::QGeometry *geom = new Qt3DCore::QGeometry;
136
137 QByteArray vertexBufferData( reinterpret_cast<const char *>( vertices.data() ), static_cast<qsizetype>( vertices.size() * sizeof( float ) ) );
138
139 Qt3DCore::QBuffer *vertexBuffer = new Qt3DCore::QBuffer( geom );
140 vertexBuffer->setData( vertexBufferData );
141
142 const int vertexFloats = 3 + ( hasNormals ? 3 : 0 ) + ( hasTexCoords ? 2 : 0 );
143 const int stride = vertexFloats * static_cast<int>( sizeof( float ) );
144 const uint vertCount = static_cast<uint>( vertices.size() / vertexFloats );
145
146 Qt3DRender::QAbstractTexture *diffuseTex = nullptr;
147 if ( matId >= 0 && matId < static_cast<int>( materials.size() ) )
148 {
149 const std::string &texName = materials[matId].diffuse_texname;
150 if ( !texName.empty() )
151 {
152 const QString texPath = QDir( objDir ).filePath( QString::fromStdString( texName ) );
153 const QImage img( texPath );
154 if ( !img.isNull() )
155 {
156 Qt3DRender::QTexture2D *texture = new Qt3DRender::QTexture2D;
157 texture->wrapMode()->setX( Qt3DRender::QTextureWrapMode::Repeat );
158 texture->wrapMode()->setY( Qt3DRender::QTextureWrapMode::Repeat );
159 texture->setFormat( Qt3DRender::QAbstractTexture::SRGB8_Alpha8 );
160 texture->addTextureImage( new QgsImageTexture( img ) );
161 Qgs3DUtils::setTextureFiltering( texture, materialContext );
162 diffuseTex = texture;
163 }
164 else
165 {
166 QgsDebugMsgLevel( u"OBJ texture not found or unreadable: '%1'"_s.arg( texPath ), 2 );
167 }
168 }
169 }
170
171 int byteOffset = 0;
172
173 Qt3DCore::QAttribute *posAttr = new Qt3DCore::QAttribute;
174 posAttr->setName( Qt3DCore::QAttribute::defaultPositionAttributeName() );
175 posAttr->setVertexBaseType( Qt3DCore::QAttribute::Float );
176 posAttr->setVertexSize( 3 );
177 posAttr->setAttributeType( Qt3DCore::QAttribute::VertexAttribute );
178 posAttr->setBuffer( vertexBuffer );
179 posAttr->setByteStride( stride );
180 posAttr->setByteOffset( byteOffset );
181 posAttr->setCount( vertCount );
182 geom->addAttribute( posAttr );
183 byteOffset += 3 * static_cast<int>( sizeof( float ) );
184
185 if ( hasNormals )
186 {
187 Qt3DCore::QAttribute *normalAttr = new Qt3DCore::QAttribute;
188 normalAttr->setName( Qt3DCore::QAttribute::defaultNormalAttributeName() );
189 normalAttr->setVertexBaseType( Qt3DCore::QAttribute::Float );
190 normalAttr->setVertexSize( 3 );
191 normalAttr->setAttributeType( Qt3DCore::QAttribute::VertexAttribute );
192 normalAttr->setBuffer( vertexBuffer );
193 normalAttr->setByteStride( stride );
194 normalAttr->setByteOffset( byteOffset );
195 normalAttr->setCount( vertCount );
196 geom->addAttribute( normalAttr );
197 byteOffset += 3 * static_cast<int>( sizeof( float ) );
198 }
199
200 if ( hasTexCoords && diffuseTex )
201 {
202 Qt3DCore::QAttribute *texAttr = new Qt3DCore::QAttribute;
203 texAttr->setName( Qt3DCore::QAttribute::defaultTextureCoordinateAttributeName() );
204 texAttr->setVertexBaseType( Qt3DCore::QAttribute::Float );
205 texAttr->setVertexSize( 2 );
206 texAttr->setAttributeType( Qt3DCore::QAttribute::VertexAttribute );
207 texAttr->setBuffer( vertexBuffer );
208 texAttr->setByteStride( stride );
209 texAttr->setByteOffset( byteOffset );
210 texAttr->setCount( vertCount );
211 geom->addAttribute( texAttr );
212 }
213
214 QgsMaterial *mat = nullptr;
215 if ( diffuseTex )
216 {
217 QgsPhongTexturedMaterial *texMat = new QgsPhongTexturedMaterial();
218 texMat->setDiffuseTexture( diffuseTex );
219 mat = texMat;
220 }
221 else if ( matId >= 0 && matId < static_cast<int>( materials.size() ) )
222 {
223 const tinyobj::material_t &m = materials[matId];
224 QgsPhongMaterial *phong = new QgsPhongMaterial();
225 // we explicitly avoid setting the ambient color, it is defined as white in most MTL files
226 // ideally, we should be using PBR material, rather than Phong
227 phong->setDiffuse( QColor::fromRgbF( m.diffuse[0], m.diffuse[1], m.diffuse[2] ) );
228 phong->setSpecular( QColor::fromRgbF( m.specular[0], m.specular[1], m.specular[2] ) );
229 phong->setShininess( m.shininess );
230 phong->setOpacity( m.dissolve );
231 mat = phong;
232 }
233
234 result.push_back( QgsMeshNodeData { std::unique_ptr<Qt3DCore::QGeometry>( geom ), std::unique_ptr<QgsMaterial>( mat ), QMatrix4x4() } );
235 }
236
237 return result;
238}
239
static void setTextureFiltering(Qt3DRender::QAbstractTexture *texture, const QgsMaterialContext &context)
Sets the default filtering options for a texture.
Holds an image that can be used as a texture in the 3D view.
Context settings for a material.
Base class for all materials used within QGIS 3D views.
Definition qgsmaterial.h:40
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:80
Pairs a Qt3D geometry with its material and transform.
Definition qgs3dutils.h:71