Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
gltfmeshbuilder.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
6
7#include <QtQuick3DGltf/private/qssggltfaccessorreader_p.h>
8#include <QtQuick3DGltf/private/qssggltfparser_p.h>
9
10#include <QtGui/qvector2d.h>
11#include <QtGui/qvector3d.h>
12#include <QtGui/qvector4d.h>
13
15
16using namespace QSSGGltf;
17
18namespace GltfMeshBuilder {
19
20namespace {
21
22struct IntVector4D {
23 qint32 x = 0;
24 qint32 y = 0;
25 qint32 z = 0;
26 qint32 w = 0;
27};
28
29struct MorphTargetData {
30 QVector<QVector3D> positions;
31 QVector<QVector3D> normals;
32 QVector<QVector3D> tangents;
33};
34
35struct PrimitiveData {
36 QVector<quint32> indexes; // triangle list, relative to this primitive's vertices
37 QVector<QVector3D> positions;
38 QVector<QVector3D> normals;
39 QVector<QVector3D> tangents;
40 QVector<QVector3D> binormals;
41 QVector<QVector2D> uv0;
42 QVector<QVector2D> uv1;
43 QVector<QVector4D> colors;
44 QVector<IntVector4D> joints;
45 QVector<QVector4D> weights;
46 QVector<MorphTargetData> targets;
47 QString materialName;
48};
49
50struct Requirements {
51 bool needsNormals = false;
52 bool needsTangents = false;
53 bool needsUV0 = false;
54 bool needsUV1 = false;
55 bool needsColors = false;
56 bool needsJoints = false;
57 int numMorphTargets = 0;
58 bool needsTargetPositions = false;
59 bool needsTargetNormals = false;
60 bool needsTargetTangents = false;
61
62 void collect(const PrimitiveData &primitive)
63 {
64 needsNormals |= !primitive.normals.isEmpty();
65 needsTangents |= !primitive.tangents.isEmpty();
66 needsUV0 |= !primitive.uv0.isEmpty();
67 needsUV1 |= !primitive.uv1.isEmpty();
68 needsColors |= !primitive.colors.isEmpty();
69 needsJoints |= !primitive.joints.isEmpty();
70 numMorphTargets = qMax(numMorphTargets, int(primitive.targets.size()));
71 for (const MorphTargetData &target : primitive.targets) {
72 needsTargetPositions |= !target.positions.isEmpty();
73 needsTargetNormals |= !target.normals.isEmpty();
74 needsTargetTangents |= !target.tangents.isEmpty();
75 }
76 }
77};
78
79// Drops degenerate strip triangles and an incomplete trailing triangle
80QVector<quint32> toTriangleList(int mode, QVector<quint32> indexes)
81{
82 if (mode == MeshPrimitive::Triangles) {
83 if (const qsizetype excess = indexes.size() % 3) {
84 qCWarning(lcQuick3DGltf) << "Triangle list has" << indexes.size()
85 << "indices, which is not a multiple of 3";
86 indexes.resize(indexes.size() - excess);
87 }
88 return indexes;
89 }
90
91 QVector<quint32> result;
92 if (mode == MeshPrimitive::TriangleStrip) {
93 result.reserve(qMax(0, (indexes.size() - 2)) * 3);
94 for (qsizetype i = 0; i + 2 < indexes.size(); ++i) {
95 quint32 a = indexes[i], b = indexes[i + 1], c = indexes[i + 2];
96 if (i % 2)
97 std::swap(a, b);
98 if (a == b || b == c || a == c)
99 continue;
100 result << a << b << c;
101 }
102 } else if (mode == MeshPrimitive::TriangleFan) {
103 result.reserve(qMax(0, (indexes.size() - 2)) * 3);
104 for (qsizetype i = 1; i + 1 < indexes.size(); ++i)
105 result << indexes[0] << indexes[i] << indexes[i + 1];
106 }
107 return result;
108}
109
110template<typename Vec>
111QVector<Vec> toVectors(const QList<float> &floats, int components)
112{
113 QVector<Vec> result;
114 const qsizetype count = floats.size() / components;
115 result.reserve(count);
116 for (qsizetype i = 0; i < count; ++i) {
117 Vec v;
118 for (int c = 0; c < qMin(components, int(sizeof(Vec) / sizeof(float))); ++c)
119 v[c] = floats[i * components + c];
120 result.append(v);
121 }
122 return result;
123}
124
125template<typename T>
126QVector<T> deindex(const QVector<T> &data, const QVector<quint32> &indexes)
127{
128 if (data.isEmpty())
129 return {};
130 QVector<T> result;
131 result.reserve(indexes.size());
132 for (const quint32 index : indexes)
133 result.append(data.value(index));
134 return result;
135}
136
137// Expands the primitive so every triangle has unique vertices
138void deindexPrimitive(PrimitiveData &primitive)
139{
140 primitive.positions = deindex(primitive.positions, primitive.indexes);
141 primitive.normals = deindex(primitive.normals, primitive.indexes);
142 primitive.tangents = deindex(primitive.tangents, primitive.indexes);
143 primitive.binormals = deindex(primitive.binormals, primitive.indexes);
144 primitive.uv0 = deindex(primitive.uv0, primitive.indexes);
145 primitive.uv1 = deindex(primitive.uv1, primitive.indexes);
146 primitive.colors = deindex(primitive.colors, primitive.indexes);
147 primitive.joints = deindex(primitive.joints, primitive.indexes);
148 primitive.weights = deindex(primitive.weights, primitive.indexes);
149 for (MorphTargetData &target : primitive.targets) {
150 target.positions = deindex(target.positions, primitive.indexes);
151 target.normals = deindex(target.normals, primitive.indexes);
152 target.tangents = deindex(target.tangents, primitive.indexes);
153 }
154 std::iota(primitive.indexes.begin(), primitive.indexes.end(), 0);
155}
156
157void generateNormals(PrimitiveData &primitive, bool smooth)
158{
159 if (!smooth) {
160 // Flat normals need per-face vertices
161 deindexPrimitive(primitive);
162 }
163
164 primitive.normals.fill(QVector3D(), primitive.positions.size());
165 for (qsizetype i = 0; i + 2 < primitive.indexes.size(); i += 3) {
166 const quint32 i0 = primitive.indexes[i], i1 = primitive.indexes[i + 1], i2 = primitive.indexes[i + 2];
167 const QVector3D faceNormal = QVector3D::crossProduct(primitive.positions[i1] - primitive.positions[i0],
168 primitive.positions[i2] - primitive.positions[i0]);
169 // Area weighted
170 primitive.normals[i0] += faceNormal;
171 primitive.normals[i1] += faceNormal;
172 primitive.normals[i2] += faceNormal;
173 }
174 for (QVector3D &normal : primitive.normals)
175 normal.normalize();
176}
177
178// Per corner, since a UV mirror seam gives a vertex two tangents
179void generateTangents(PrimitiveData &primitive)
180{
181 if (primitive.uv0.isEmpty() || primitive.normals.isEmpty())
182 return;
183 deindexPrimitive(primitive);
184
185 const qsizetype cornerCount = primitive.indexes.size();
186 if (cornerCount == 0)
187 return;
188
189 // xyz tangent, w bitangent sign
190 QVector<float> generated(cornerCount * 4);
191 QSSGMesh::generateTangents(generated.data(), primitive.indexes.constData(), size_t(cornerCount),
192 reinterpret_cast<const float *>(primitive.positions.constData()),
193 size_t(primitive.positions.size()), sizeof(QVector3D),
194 reinterpret_cast<const float *>(primitive.normals.constData()), sizeof(QVector3D),
195 reinterpret_cast<const float *>(primitive.uv0.constData()), sizeof(QVector2D));
196
197 primitive.tangents.resize(cornerCount);
198 primitive.binormals.resize(cornerCount);
199 for (qsizetype i = 0; i < cornerCount; ++i) {
200 const float *t = generated.constData() + i * 4;
201 const QVector3D tangent(t[0], t[1], t[2]);
202 primitive.tangents[i] = tangent;
203 primitive.binormals[i] = QVector3D::crossProduct(primitive.normals.at(i), tangent) * t[3];
204 }
205}
206
207bool materialNeedsTangents(const QSSGGltfDocument &document, int materialIndex)
208{
209 if (materialIndex < 0 || materialIndex >= document.materials.size())
210 return false;
211 const Material &material = document.materials.at(materialIndex);
212 // Anisotropy needs a tangent frame that follows the UVs
213 return material.normalTexture.isSet()
214 || (material.clearcoat && material.clearcoat->clearcoatNormalTexture.isSet())
215 || (material.anisotropy && material.anisotropy->anisotropyStrength > 0.0f);
216}
217
218// Equivalent of Assimp's joinIdenticalVertices
219void weldVertices(PrimitiveData &primitive)
220{
221 const qsizetype vertexCount = primitive.positions.size();
222 if (vertexCount == 0)
223 return;
224
225 QVarLengthArray<QSSGMesh::MeshVertexStream, 16> streams;
226 const auto addStream = [&](const auto &data) {
227 using Element = std::decay_t<decltype(*data.constData())>;
228 if (!data.isEmpty())
229 streams.append({ data.constData(), sizeof(Element), sizeof(Element) });
230 };
231 addStream(primitive.positions);
232 addStream(primitive.normals);
233 addStream(primitive.tangents);
234 addStream(primitive.binormals);
235 addStream(primitive.uv0);
236 addStream(primitive.uv1);
237 addStream(primitive.colors);
238 addStream(primitive.joints);
239 addStream(primitive.weights);
240 for (const MorphTargetData &target : std::as_const(primitive.targets)) {
241 addStream(target.positions);
242 addStream(target.normals);
243 addStream(target.tangents);
244 }
245
246 // Welding is only an optimization, so skip it instead
247 if (size_t(streams.size()) > QSSGMesh::maxVertexStreams) {
248 qCWarning(lcQuick3DGltf) << "Not welding vertices for a mesh with" << streams.size()
249 << "attribute streams; at most" << QSSGMesh::maxVertexStreams
250 << "are supported";
251 return;
252 }
253
254 QVector<unsigned int> remap(vertexCount);
255 const size_t uniqueCount = QSSGMesh::generateVertexRemap(remap.data(), primitive.indexes.constData(),
256 primitive.indexes.size(), vertexCount,
257 streams.constData(), streams.size());
258 if (qsizetype(uniqueCount) == vertexCount)
259 return;
260
261 const auto remapStream = [&](auto &data) {
262 using Element = std::decay_t<decltype(*data.constData())>;
263 if (data.isEmpty())
264 return;
265 QVector<Element> welded(uniqueCount);
266 QSSGMesh::remapVertexBuffer(welded.data(), data.constData(), vertexCount, sizeof(Element), remap.constData());
267 data = std::move(welded);
268 };
269 remapStream(primitive.positions);
270 remapStream(primitive.normals);
271 remapStream(primitive.tangents);
272 remapStream(primitive.binormals);
273 remapStream(primitive.uv0);
274 remapStream(primitive.uv1);
275 remapStream(primitive.colors);
276 remapStream(primitive.joints);
277 remapStream(primitive.weights);
278 for (MorphTargetData &target : primitive.targets) {
279 remapStream(target.positions);
280 remapStream(target.normals);
281 remapStream(target.tangents);
282 }
283
284 QSSGMesh::remapIndexBuffer(primitive.indexes.data(), primitive.indexes.constData(),
285 primitive.indexes.size(), remap.constData());
286}
287
288bool loadPrimitive(const QSSGGltfDocument &document, const MeshPrimitive &source,
289 const GltfSceneConverter::Options &options, PrimitiveData &primitive)
290{
291 const int positionAccessor = source.attributes.value(QByteArrayLiteral("POSITION"), -1);
292 if (positionAccessor < 0) {
293 qCWarning(lcQuick3DGltf) << "Skipping primitive without POSITION data";
294 return false;
295 }
296
297 primitive.positions = toVectors<QVector3D>(QSSGGltfAccessorReader::readAsFloats(document, positionAccessor), 3);
298 const qsizetype vertexCount = primitive.positions.size();
299
300 // Indices; non-indexed primitives get a sequential index buffer
301 QVector<quint32> indexes;
302 if (source.indices >= 0) {
303 indexes = QVector<quint32>(QSSGGltfAccessorReader::readIndices(document, source.indices));
304 } else {
305 indexes.resize(vertexCount);
306 std::iota(indexes.begin(), indexes.end(), 0);
307 }
308 primitive.indexes = toTriangleList(source.mode, std::move(indexes));
309 if (primitive.indexes.isEmpty()) {
310 if (source.mode != MeshPrimitive::Triangles && source.mode != MeshPrimitive::TriangleStrip
311 && source.mode != MeshPrimitive::TriangleFan) {
312 qCWarning(lcQuick3DGltf) << "Skipping primitive with unsupported mode" << source.mode
313 << "(points and lines are not supported)";
314 }
315 return false;
316 }
317
318 // Everything below indexes the vertex data directly
319 for (const quint32 index : std::as_const(primitive.indexes)) {
320 if (qsizetype(index) >= vertexCount) {
321 qCWarning(lcQuick3DGltf) << "Skipping primitive with out-of-range index value" << index;
322 return false;
323 }
324 }
325
326 // TANGENT needs the normals, so NORMAL comes first
327 static const QByteArray knownSemantics[] = {
328 QByteArrayLiteral("POSITION"), QByteArrayLiteral("NORMAL"), QByteArrayLiteral("TANGENT"),
329 QByteArrayLiteral("TEXCOORD_0"), QByteArrayLiteral("TEXCOORD_1"), QByteArrayLiteral("COLOR_0"),
330 QByteArrayLiteral("JOINTS_0"), QByteArrayLiteral("WEIGHTS_0"),
331 };
332 for (auto it = source.attributes.constBegin(); it != source.attributes.constEnd(); ++it) {
333 if (std::find(std::begin(knownSemantics), std::end(knownSemantics), it.key()) == std::end(knownSemantics)) {
334 qCWarning(lcQuick3DGltf) << "Ignoring unsupported vertex attribute" << it.key();
335 }
336 }
337
338 for (const QByteArray &semantic : knownSemantics) {
339 const int accessorIndex = source.attributes.value(semantic, -1);
340 if (accessorIndex < 0 || accessorIndex >= document.accessors.size())
341 continue;
342 const Accessor &accessor = document.accessors.at(accessorIndex);
343 const auto floats = [&] { return QSSGGltfAccessorReader::readAsFloats(document, accessorIndex); };
344
345 if (semantic == QByteArrayLiteral("POSITION")) {
346 // already read
347 } else if (semantic == QByteArrayLiteral("NORMAL")) {
348 primitive.normals = toVectors<QVector3D>(floats(), 3);
349 } else if (semantic == QByteArrayLiteral("TANGENT")) {
350 // The specification requires ignoring tangents without normals
351 if (primitive.normals.isEmpty())
352 continue;
353 // xyz tangent, w handedness
354 const QList<float> data = floats();
355 const qsizetype count = data.size() / 4;
356 primitive.tangents.reserve(count);
357 primitive.binormals.reserve(count);
358 for (qsizetype i = 0; i < count; ++i) {
359 const QVector3D tangent(data[i * 4], data[i * 4 + 1], data[i * 4 + 2]);
360 const float handedness = data[i * 4 + 3];
361 primitive.tangents.append(tangent);
362 const QVector3D normal = primitive.normals.value(i);
363 primitive.binormals.append(QVector3D::crossProduct(normal, tangent) * handedness);
364 }
365 } else if (semantic == QByteArrayLiteral("TEXCOORD_0") || semantic == QByteArrayLiteral("TEXCOORD_1")) {
366 // glTF UV origin is top-left, Qt Quick 3D bottom-left
367 auto uv = toVectors<QVector2D>(floats(), Accessor::componentCount(accessor.type));
368 for (QVector2D &coord : uv)
369 coord.setY(1.0f - coord.y());
370 if (semantic == QByteArrayLiteral("TEXCOORD_0"))
371 primitive.uv0 = std::move(uv);
372 else
373 primitive.uv1 = std::move(uv);
374 } else if (semantic == QByteArrayLiteral("COLOR_0")) {
375 const int components = Accessor::componentCount(accessor.type);
376 if (components == 3) {
377 const QList<float> data = floats();
378 const qsizetype count = data.size() / 3;
379 primitive.colors.reserve(count);
380 for (qsizetype i = 0; i < count; ++i)
381 primitive.colors.append(QVector4D(data[i * 3], data[i * 3 + 1], data[i * 3 + 2], 1.0f));
382 } else {
383 primitive.colors = toVectors<QVector4D>(floats(), 4);
384 }
385 } else if (semantic == QByteArrayLiteral("JOINTS_0")) {
386 const QList<float> data = floats();
387 const qsizetype count = data.size() / 4;
388 primitive.joints.reserve(count);
389 // Also maps NaN, whose cast would be undefined, to 0
390 const auto toJointIndex = [](float value) {
391 return value >= 0.0f && value < 2.0e9f ? qint32(value) : 0;
392 };
393 for (qsizetype i = 0; i < count; ++i) {
394 primitive.joints.append({ toJointIndex(data[i * 4]), toJointIndex(data[i * 4 + 1]),
395 toJointIndex(data[i * 4 + 2]), toJointIndex(data[i * 4 + 3]) });
396 }
397 } else if (semantic == QByteArrayLiteral("WEIGHTS_0")) {
398 auto weights = toVectors<QVector4D>(floats(), 4);
399 // Renormalize so the four influences sum to one
400 for (QVector4D &weight : weights) {
401 const float sum = weight.x() + weight.y() + weight.z() + weight.w();
402 if (!qFuzzyIsNull(sum) && !qFuzzyCompare(sum, 1.0f))
403 weight /= sum;
404 }
405 primitive.weights = std::move(weights);
406 }
407 }
408
409 // The renderer supports at most 8 morph targets
410 constexpr int maxMorphTargets = 8;
411 if (source.targets.size() > maxMorphTargets) {
412 qCWarning(lcQuick3DGltf) << "Mesh primitive has" << source.targets.size()
413 << "morph targets, only the first" << maxMorphTargets << "are used";
414 }
415 const qsizetype targetCount = qMin(qsizetype(maxMorphTargets), source.targets.size());
416 for (qsizetype targetIndex = 0; targetIndex < targetCount; ++targetIndex) {
417 const QHash<QByteArray, int> &sourceTarget = source.targets.at(targetIndex);
418 MorphTargetData target;
419 if (const int accessor = sourceTarget.value(QByteArrayLiteral("POSITION"), -1); accessor >= 0)
420 target.positions = toVectors<QVector3D>(QSSGGltfAccessorReader::readAsFloats(document, accessor), 3);
421 if (const int accessor = sourceTarget.value(QByteArrayLiteral("NORMAL"), -1); accessor >= 0)
422 target.normals = toVectors<QVector3D>(QSSGGltfAccessorReader::readAsFloats(document, accessor), 3);
423 if (const int accessor = sourceTarget.value(QByteArrayLiteral("TANGENT"), -1); accessor >= 0)
424 target.tangents = toVectors<QVector3D>(QSSGGltfAccessorReader::readAsFloats(document, accessor), 3);
425 primitive.targets.append(target);
426 }
427
428 // Everything below relies on matching attribute counts
429 const auto normalizeCount = [vertexCount](auto &data, const char *what) {
430 if (!data.isEmpty() && data.size() != vertexCount) {
431 qCWarning(lcQuick3DGltf) << what << "data has" << data.size() << "elements, expected" << vertexCount;
432 data.resize(vertexCount);
433 }
434 };
435 normalizeCount(primitive.normals, "NORMAL");
436 normalizeCount(primitive.tangents, "TANGENT");
437 normalizeCount(primitive.binormals, "TANGENT");
438 normalizeCount(primitive.uv0, "TEXCOORD_0");
439 normalizeCount(primitive.uv1, "TEXCOORD_1");
440 normalizeCount(primitive.colors, "COLOR_0");
441 normalizeCount(primitive.joints, "JOINTS_0");
442 normalizeCount(primitive.weights, "WEIGHTS_0");
443 for (MorphTargetData &target : primitive.targets) {
444 normalizeCount(target.positions, "morph target POSITION");
445 normalizeCount(target.normals, "morph target NORMAL");
446 normalizeCount(target.tangents, "morph target TANGENT");
447 }
448
449 if (primitive.normals.isEmpty())
450 generateNormals(primitive, options.generateSmoothNormals);
451
452 // Switchable variants need tangents if any of their materials does
453 const auto anyVariantNeedsTangents = [&] {
454 if (!options.materialVariant.isEmpty() || document.materialVariants.isEmpty())
455 return false;
456 for (const MeshPrimitive::VariantMapping &mapping : source.variantMappings) {
457 if (materialNeedsTangents(document, mapping.material))
458 return true;
459 }
460 return false;
461 };
462 if (primitive.tangents.isEmpty()
463 && (options.forceTangentGeneration
464 || materialNeedsTangents(document, source.effectiveMaterial(options.materialVariantIndex))
465 || anyVariantNeedsTangents())) {
466 generateTangents(primitive);
467 }
468
469 // glTF targets are deltas, the renderer expects absolute values
470 for (MorphTargetData &target : primitive.targets) {
471 for (qsizetype i = 0; i < target.positions.size() && i < primitive.positions.size(); ++i)
472 target.positions[i] += primitive.positions.at(i);
473 for (qsizetype i = 0; i < target.normals.size() && i < primitive.normals.size(); ++i)
474 target.normals[i] += primitive.normals.at(i);
475 for (qsizetype i = 0; i < target.tangents.size() && i < primitive.tangents.size(); ++i)
476 target.tangents[i] += primitive.tangents.at(i);
477 }
478
479 if (options.joinIdenticalVertices)
480 weldVertices(primitive);
481
482 if (const int materialIndex = source.effectiveMaterial(options.materialVariantIndex); materialIndex >= 0)
483 primitive.materialName = document.materials.at(materialIndex).name;
484
485 QSSGMesh::optimizeVertexCache(primitive.indexes.data(), primitive.indexes.data(),
486 primitive.indexes.size(), primitive.positions.size());
487 return true;
488}
489
490template<typename T>
491void appendVertexData(QByteArray &buffer, const QVector<T> &data, qsizetype vertexCount)
492{
493 if (!data.isEmpty())
494 buffer.append(reinterpret_cast<const char *>(data.constData()), data.size() * sizeof(T));
495 // Zero-fill so all attributes cover the same vertex range
496 if (data.size() < vertexCount)
497 buffer.append((vertexCount - data.size()) * sizeof(T), '\0');
498}
499
500} // namespace
501
502/*!
503 \internal
504
505 Builds a QSSGMesh::Mesh from the glTF mesh \a mesh in \a document, with
506 one subset per triangle primitive. Primitives with non-triangle topology
507 that cannot be converted (points and lines) are skipped with a warning.
508 Returns an invalid mesh when no primitive could be converted. When
509 \a usedPrimitives is given, it receives the indices of the source
510 primitives that became subsets, in subset order, so callers can build a
511 matching material list. \a morphInfo receives the number of morph targets
512 stored in the mesh and which attributes they carry.
513*/
514QSSGMesh::Mesh buildMesh(const QSSGGltfDocument &document, const QSSGGltf::Mesh &mesh,
515 const GltfSceneConverter::Options &options,
516 QList<int> *usedPrimitives,
517 GltfMorphTargetInfo *morphInfo)
518{
519 QVector<PrimitiveData> primitives;
520 primitives.reserve(mesh.primitives.size());
521 Requirements requirements;
522 for (qsizetype primitiveIndex = 0; primitiveIndex < mesh.primitives.size(); ++primitiveIndex) {
523 PrimitiveData primitive;
524 if (loadPrimitive(document, mesh.primitives.at(primitiveIndex), options, primitive)) {
525 requirements.collect(primitive);
526 primitives.append(std::move(primitive));
527 if (usedPrimitives)
528 usedPrimitives->append(int(primitiveIndex));
529 }
530 }
531
532 if (primitives.isEmpty())
533 return {};
534
535 QByteArray indexBufferData;
536 QByteArray positionData;
537 QByteArray normalData;
538 QByteArray tangentData;
539 QByteArray binormalData;
540 QByteArray uv0Data;
541 QByteArray uv1Data;
542 QByteArray colorData;
543 QByteArray jointData;
544 QByteArray weightData;
545 // vec3 padded to vec4, one buffer per target and component
546 struct TargetBuffers {
547 QByteArray positions;
548 QByteArray normals;
549 QByteArray tangents;
550 };
551 QVector<TargetBuffers> targetData(requirements.numMorphTargets);
552 const auto appendPaddedVec3 = [](QByteArray &buffer, const QVector<QVector3D> &data, qsizetype vertexCount) {
553 for (const QVector3D &v : data) {
554 const float padded[4] = { v.x(), v.y(), v.z(), 0.0f };
555 buffer.append(reinterpret_cast<const char *>(padded), sizeof(padded));
556 }
557 if (data.size() < vertexCount)
558 buffer.append((vertexCount - data.size()) * 4 * sizeof(float), '\0');
559 };
560 QVector<QSSGMesh::AssetMeshSubset> subsets;
561
562 // 32-bit, since Metal needs 4 byte aligned index buffer offsets
563 const auto indexType = QSSGMesh::Mesh::ComponentType::UnsignedInt32;
564
565 const auto appendCopy = [](auto &data, qsizetype sourceIndex) {
566 if (!data.isEmpty())
567 data.append(data.at(sourceIndex));
568 };
569
570 quint32 baseVertex = 0;
571 for (PrimitiveData &primitive : primitives) {
572 quint32 indexOffset = indexBufferData.size() / QSSGMesh::MeshInternal::byteSizeForComponentType(indexType);
573
574 // LOD indexes precede the original ones, lowest quality first
575 QVector<QSSGMesh::Mesh::Lod> meshLods;
576 QVector<quint32> lodIndexes;
577 if (options.generateMeshLODs && !primitive.normals.isEmpty()) {
578 QVector<QSSGMesh::MeshVertexSplit> splitVertices;
579 const auto lods = QSSGMesh::generateMeshLevelsOfDetail(primitive.positions, primitive.normals,
580 primitive.indexes, splitVertices,
581 options.lodNormalMergeAngle,
582 options.lodNormalSplitAngle);
583 // Split vertices are copies with a recalculated normal
584 for (const QSSGMesh::MeshVertexSplit &split : splitVertices) {
585 primitive.positions.append(primitive.positions.at(split.sourceIndex));
586 primitive.normals.append(split.normal);
587 appendCopy(primitive.tangents, split.sourceIndex);
588 appendCopy(primitive.binormals, split.sourceIndex);
589 appendCopy(primitive.uv0, split.sourceIndex);
590 appendCopy(primitive.uv1, split.sourceIndex);
591 appendCopy(primitive.colors, split.sourceIndex);
592 appendCopy(primitive.joints, split.sourceIndex);
593 appendCopy(primitive.weights, split.sourceIndex);
594 for (MorphTargetData &target : primitive.targets) {
595 appendCopy(target.positions, split.sourceIndex);
596 appendCopy(target.normals, split.sourceIndex);
597 appendCopy(target.tangents, split.sourceIndex);
598 }
599 }
600 for (const QSSGMesh::MeshLevelOfDetail &lodEntry : lods) {
601 QSSGMesh::Mesh::Lod lod;
602 lod.offset = indexOffset;
603 lod.count = lodEntry.indexes.size();
604 lod.distance = lodEntry.distance;
605 meshLods.prepend(lod);
606 indexOffset += lod.count;
607 QVector<quint32> currentLodIndexes = lodEntry.indexes;
608 QSSGMesh::optimizeVertexCache(currentLodIndexes.data(), currentLodIndexes.data(),
609 currentLodIndexes.size(), primitive.positions.size());
610 lodIndexes += currentLodIndexes;
611 }
612 }
613 const qsizetype vertexCount = primitive.positions.size();
614
615 QVector<quint32> globalIndexes = lodIndexes + primitive.indexes;
616 primitive.indexes.clear();
617 for (quint32 &index : globalIndexes)
618 index += baseVertex;
619 indexBufferData.append(reinterpret_cast<const char *>(globalIndexes.constData()),
620 globalIndexes.size() * sizeof(quint32));
621 const quint32 subsetIndexCount = globalIndexes.size() - lodIndexes.size();
622
623 appendVertexData(positionData, primitive.positions, vertexCount);
624 if (requirements.needsNormals)
625 appendVertexData(normalData, primitive.normals, vertexCount);
626 if (requirements.needsTangents) {
627 appendVertexData(tangentData, primitive.tangents, vertexCount);
628 appendVertexData(binormalData, primitive.binormals, vertexCount);
629 }
630 if (requirements.needsUV0)
631 appendVertexData(uv0Data, primitive.uv0, vertexCount);
632 if (requirements.needsUV1)
633 appendVertexData(uv1Data, primitive.uv1, vertexCount);
634 if (requirements.needsColors)
635 appendVertexData(colorData, primitive.colors, vertexCount);
636 if (requirements.needsJoints) {
637 if (options.useFloatJointIndices) {
638 QVector<QVector4D> floatJoints;
639 floatJoints.reserve(primitive.joints.size());
640 for (const IntVector4D &joint : std::as_const(primitive.joints))
641 floatJoints.append(QVector4D(float(joint.x), float(joint.y), float(joint.z), float(joint.w)));
642 appendVertexData(jointData, floatJoints, vertexCount);
643 } else {
644 appendVertexData(jointData, primitive.joints, vertexCount);
645 }
646 appendVertexData(weightData, primitive.weights, vertexCount);
647 }
648
649 for (int targetIndex = 0; targetIndex < requirements.numMorphTargets; ++targetIndex) {
650 const MorphTargetData target = primitive.targets.value(targetIndex);
651 if (requirements.needsTargetPositions)
652 appendPaddedVec3(targetData[targetIndex].positions, target.positions, vertexCount);
653 if (requirements.needsTargetNormals)
654 appendPaddedVec3(targetData[targetIndex].normals, target.normals, vertexCount);
655 if (requirements.needsTargetTangents)
656 appendPaddedVec3(targetData[targetIndex].tangents, target.tangents, vertexCount);
657 }
658
659 subsets.append({ primitive.materialName,
660 subsetIndexCount,
661 indexOffset,
662 0, // the builder calculates the bounds from the position data
663 0, 0, // lightmap size hint
664 meshLods });
665 baseVertex += vertexCount;
666 }
667
668 QVector<QSSGMesh::AssetVertexEntry> entries;
669 entries.append({ QSSGMesh::MeshInternal::getPositionAttrName(), positionData,
670 QSSGMesh::Mesh::ComponentType::Float32, 3 });
671 if (!normalData.isEmpty()) {
672 entries.append({ QSSGMesh::MeshInternal::getNormalAttrName(), normalData,
673 QSSGMesh::Mesh::ComponentType::Float32, 3 });
674 }
675 if (!uv0Data.isEmpty()) {
676 entries.append({ QSSGMesh::MeshInternal::getUV0AttrName(), uv0Data,
677 QSSGMesh::Mesh::ComponentType::Float32, 2 });
678 }
679 if (!uv1Data.isEmpty()) {
680 entries.append({ QSSGMesh::MeshInternal::getUV1AttrName(), uv1Data,
681 QSSGMesh::Mesh::ComponentType::Float32, 2 });
682 }
683 if (!tangentData.isEmpty()) {
684 entries.append({ QSSGMesh::MeshInternal::getTexTanAttrName(), tangentData,
685 QSSGMesh::Mesh::ComponentType::Float32, 3 });
686 entries.append({ QSSGMesh::MeshInternal::getTexBinormalAttrName(), binormalData,
687 QSSGMesh::Mesh::ComponentType::Float32, 3 });
688 }
689 if (!colorData.isEmpty()) {
690 entries.append({ QSSGMesh::MeshInternal::getColorAttrName(), colorData,
691 QSSGMesh::Mesh::ComponentType::Float32, 4 });
692 }
693 if (!jointData.isEmpty()) {
694 entries.append({ QSSGMesh::MeshInternal::getJointAttrName(), jointData,
695 options.useFloatJointIndices ? QSSGMesh::Mesh::ComponentType::Float32
696 : QSSGMesh::Mesh::ComponentType::Int32, 4 });
697 entries.append({ QSSGMesh::MeshInternal::getWeightAttrName(), weightData,
698 QSSGMesh::Mesh::ComponentType::Float32, 4 });
699 }
700
701 // Grouped per target, as Mesh::fromAssetData expects
702 int numTargetComps = 0;
703 if (requirements.numMorphTargets > 0) {
704 numTargetComps = int(requirements.needsTargetPositions) + int(requirements.needsTargetNormals)
705 + int(requirements.needsTargetTangents);
706 for (int targetIndex = 0; targetIndex < requirements.numMorphTargets; ++targetIndex) {
707 if (requirements.needsTargetPositions) {
708 entries.append({ QSSGMesh::MeshInternal::getPositionAttrName(), targetData[targetIndex].positions,
709 QSSGMesh::Mesh::ComponentType::Float32, 3, targetIndex });
710 }
711 if (requirements.needsTargetNormals) {
712 entries.append({ QSSGMesh::MeshInternal::getNormalAttrName(), targetData[targetIndex].normals,
713 QSSGMesh::Mesh::ComponentType::Float32, 3, targetIndex });
714 }
715 if (requirements.needsTargetTangents) {
716 entries.append({ QSSGMesh::MeshInternal::getTexTanAttrName(), targetData[targetIndex].tangents,
717 QSSGMesh::Mesh::ComponentType::Float32, 3, targetIndex });
718 }
719 }
720 }
721
722 if (morphInfo) {
723 morphInfo->count = requirements.numMorphTargets;
724 morphInfo->hasPositions = requirements.needsTargetPositions;
725 morphInfo->hasNormals = requirements.needsTargetNormals;
726 morphInfo->hasTangents = requirements.needsTargetTangents;
727 }
728
729 return QSSGMesh::Mesh::fromAssetData(entries, indexBufferData, indexType, subsets,
730 quint32(requirements.numMorphTargets), quint32(numTargetComps));
731}
732
733} // namespace GltfMeshBuilder
734
735QT_END_NAMESPACE
QSSGMesh::Mesh buildMesh(const QSSGGltfDocument &document, const QSSGGltf::Mesh &mesh, const GltfSceneConverter::Options &options, QList< int > *usedPrimitives, GltfMorphTargetInfo *morphInfo)
Combined button and popup list for selecting options.