Revision control

++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++
++ #include "gltfmeshbuilder.h"
++
++ #include <QtQuick3DGltf/private/qssggltfaccessorreader_p.h>
++ #include <QtQuick3DGltf/private/qssggltfparser_p.h>
++
++ #include <QtGui/qvector2d.h>
++ #include <QtGui/qvector3d.h>
++ #include <QtGui/qvector4d.h>
++
++ QT_BEGIN_NAMESPACE
++
++ using namespace QSSGGltf;
++
++ namespace GltfMeshBuilder {
++
++ namespace {
++
++ struct IntVector4D {
++ qint32 x = 0;
++ qint32 y = 0;
++ qint32 z = 0;
++ qint32 w = 0;
++ };
++
++ struct MorphTargetData {
++ QVector<QVector3D> positions;
++ QVector<QVector3D> normals;
++ QVector<QVector3D> tangents;
++ };
++
++ struct PrimitiveData {
++ QVector<quint32> indexes; // triangle list, relative to this primitive's vertices
++ QVector<QVector3D> positions;
++ QVector<QVector3D> normals;
++ QVector<QVector3D> tangents;
++ QVector<QVector3D> binormals;
++ QVector<QVector2D> uv0;
++ QVector<QVector2D> uv1;
++ QVector<QVector4D> colors;
++ QVector<IntVector4D> joints;
++ QVector<QVector4D> weights;
++ QVector<MorphTargetData> targets;
++ QString materialName;
++ };
++
++ struct Requirements {
++ bool needsNormals = false;
++ bool needsTangents = false;
++ bool needsUV0 = false;
++ bool needsUV1 = false;
++ bool needsColors = false;
++ bool needsJoints = false;
++ int numMorphTargets = 0;
++ bool needsTargetPositions = false;
++ bool needsTargetNormals = false;
++ bool needsTargetTangents = false;
++
++ void collect(const PrimitiveData &primitive)
++ {
++ needsNormals |= !primitive.normals.isEmpty();
++ needsTangents |= !primitive.tangents.isEmpty();
++ needsUV0 |= !primitive.uv0.isEmpty();
++ needsUV1 |= !primitive.uv1.isEmpty();
++ needsColors |= !primitive.colors.isEmpty();
++ needsJoints |= !primitive.joints.isEmpty();
++ numMorphTargets = qMax(numMorphTargets, int(primitive.targets.size()));
++ for (const MorphTargetData &target : primitive.targets) {
++ needsTargetPositions |= !target.positions.isEmpty();
++ needsTargetNormals |= !target.normals.isEmpty();
++ needsTargetTangents |= !target.tangents.isEmpty();
++ }
++ }
++ };
++
++ // Drops degenerate strip triangles and an incomplete trailing triangle
++ QVector<quint32> toTriangleList(int mode, QVector<quint32> indexes)
++ {
++ if (mode == MeshPrimitive::Triangles) {
++ if (const qsizetype excess = indexes.size() % 3) {
++ qCWarning(lcQuick3DGltf) << "Triangle list has" << indexes.size()
++ << "indices, which is not a multiple of 3";
++ indexes.resize(indexes.size() - excess);
++ }
++ return indexes;
++ }
++
++ QVector<quint32> result;
++ if (mode == MeshPrimitive::TriangleStrip) {
++ result.reserve(qMax(0, (indexes.size() - 2)) * 3);
++ for (qsizetype i = 0; i + 2 < indexes.size(); ++i) {
++ quint32 a = indexes[i], b = indexes[i + 1], c = indexes[i + 2];
++ if (i % 2)
++ std::swap(a, b);
++ if (a == b || b == c || a == c)
++ continue;
++ result << a << b << c;
++ }
++ } else if (mode == MeshPrimitive::TriangleFan) {
++ result.reserve(qMax(0, (indexes.size() - 2)) * 3);
++ for (qsizetype i = 1; i + 1 < indexes.size(); ++i)
++ result << indexes[0] << indexes[i] << indexes[i + 1];
++ }
++ return result;
++ }
++
++ template<typename Vec>
++ QVector<Vec> toVectors(const QList<float> &floats, int components)
++ {
++ QVector<Vec> result;
++ const qsizetype count = floats.size() / components;
++ result.reserve(count);
++ for (qsizetype i = 0; i < count; ++i) {
++ Vec v;
++ for (int c = 0; c < qMin(components, int(sizeof(Vec) / sizeof(float))); ++c)
++ v[c] = floats[i * components + c];
++ result.append(v);
++ }
++ return result;
++ }
++
++ template<typename T>
++ QVector<T> deindex(const QVector<T> &data, const QVector<quint32> &indexes)
++ {
++ if (data.isEmpty())
++ return {};
++ QVector<T> result;
++ result.reserve(indexes.size());
++ for (const quint32 index : indexes)
++ result.append(data.value(index));
++ return result;
++ }
++
++ // Expands the primitive so every triangle has unique vertices
++ void deindexPrimitive(PrimitiveData &primitive)
++ {
++ primitive.positions = deindex(primitive.positions, primitive.indexes);
++ primitive.normals = deindex(primitive.normals, primitive.indexes);
++ primitive.tangents = deindex(primitive.tangents, primitive.indexes);
++ primitive.binormals = deindex(primitive.binormals, primitive.indexes);
++ primitive.uv0 = deindex(primitive.uv0, primitive.indexes);
++ primitive.uv1 = deindex(primitive.uv1, primitive.indexes);
++ primitive.colors = deindex(primitive.colors, primitive.indexes);
++ primitive.joints = deindex(primitive.joints, primitive.indexes);
++ primitive.weights = deindex(primitive.weights, primitive.indexes);
++ for (MorphTargetData &target : primitive.targets) {
++ target.positions = deindex(target.positions, primitive.indexes);
++ target.normals = deindex(target.normals, primitive.indexes);
++ target.tangents = deindex(target.tangents, primitive.indexes);
++ }
++ std::iota(primitive.indexes.begin(), primitive.indexes.end(), 0);
++ }
++
++ void generateNormals(PrimitiveData &primitive, bool smooth)
++ {
++ if (!smooth) {
++ // Flat normals need per-face vertices
++ deindexPrimitive(primitive);
++ }
++
++ primitive.normals.fill(QVector3D(), primitive.positions.size());
++ for (qsizetype i = 0; i + 2 < primitive.indexes.size(); i += 3) {
++ const quint32 i0 = primitive.indexes[i], i1 = primitive.indexes[i + 1], i2 = primitive.indexes[i + 2];
++ const QVector3D faceNormal = QVector3D::crossProduct(primitive.positions[i1] - primitive.positions[i0],
++ primitive.positions[i2] - primitive.positions[i0]);
++ // Area weighted
++ primitive.normals[i0] += faceNormal;
++ primitive.normals[i1] += faceNormal;
++ primitive.normals[i2] += faceNormal;
++ }
++ for (QVector3D &normal : primitive.normals)
++ normal.normalize();
++ }
++
++ // Per corner, since a UV mirror seam gives a vertex two tangents
++ void generateTangents(PrimitiveData &primitive)
++ {
++ if (primitive.uv0.isEmpty() || primitive.normals.isEmpty())
++ return;
++ deindexPrimitive(primitive);
++
++ const qsizetype cornerCount = primitive.indexes.size();
++ if (cornerCount == 0)
++ return;
++
++ // xyz tangent, w bitangent sign
++ QVector<float> generated(cornerCount * 4);
++ QSSGMesh::generateTangents(generated.data(), primitive.indexes.constData(), size_t(cornerCount),
++ reinterpret_cast<const float *>(primitive.positions.constData()),
++ size_t(primitive.positions.size()), sizeof(QVector3D),
++ reinterpret_cast<const float *>(primitive.normals.constData()), sizeof(QVector3D),
++ reinterpret_cast<const float *>(primitive.uv0.constData()), sizeof(QVector2D));
++
++ primitive.tangents.resize(cornerCount);
++ primitive.binormals.resize(cornerCount);
++ for (qsizetype i = 0; i < cornerCount; ++i) {
++ const float *t = generated.constData() + i * 4;
++ const QVector3D tangent(t[0], t[1], t[2]);
++ primitive.tangents[i] = tangent;
++ primitive.binormals[i] = QVector3D::crossProduct(primitive.normals.at(i), tangent) * t[3];
++ }
++ }
++
++ bool materialNeedsTangents(const QSSGGltfDocument &document, int materialIndex)
++ {
++ if (materialIndex < 0 || materialIndex >= document.materials.size())
++ return false;
++ const Material &material = document.materials.at(materialIndex);
++ // Anisotropy needs a tangent frame that follows the UVs
++ return material.normalTexture.isSet()
++ || (material.clearcoat && material.clearcoat->clearcoatNormalTexture.isSet())
++ || (material.anisotropy && material.anisotropy->anisotropyStrength > 0.0f);
++ }
++
++ // Equivalent of Assimp's joinIdenticalVertices
++ void weldVertices(PrimitiveData &primitive)
++ {
++ const qsizetype vertexCount = primitive.positions.size();
++ if (vertexCount == 0)
++ return;
++
++ QVarLengthArray<QSSGMesh::MeshVertexStream, 16> streams;
++ const auto addStream = [&](const auto &data) {
++ using Element = std::decay_t<decltype(*data.constData())>;
++ if (!data.isEmpty())
++ streams.append({ data.constData(), sizeof(Element), sizeof(Element) });
++ };
++ addStream(primitive.positions);
++ addStream(primitive.normals);
++ addStream(primitive.tangents);
++ addStream(primitive.binormals);
++ addStream(primitive.uv0);
++ addStream(primitive.uv1);
++ addStream(primitive.colors);
++ addStream(primitive.joints);
++ addStream(primitive.weights);
++ for (const MorphTargetData &target : std::as_const(primitive.targets)) {
++ addStream(target.positions);
++ addStream(target.normals);
++ addStream(target.tangents);
++ }
++
++ // Welding is only an optimization, so skip it instead
++ if (size_t(streams.size()) > QSSGMesh::maxVertexStreams) {
++ qCWarning(lcQuick3DGltf) << "Not welding vertices for a mesh with" << streams.size()
++ << "attribute streams; at most" << QSSGMesh::maxVertexStreams
++ << "are supported";
++ return;
++ }
++
++ QVector<unsigned int> remap(vertexCount);
++ const size_t uniqueCount = QSSGMesh::generateVertexRemap(remap.data(), primitive.indexes.constData(),
++ primitive.indexes.size(), vertexCount,
++ streams.constData(), streams.size());
++ if (qsizetype(uniqueCount) == vertexCount)
++ return;
++
++ const auto remapStream = [&](auto &data) {
++ using Element = std::decay_t<decltype(*data.constData())>;
++ if (data.isEmpty())
++ return;
++ QVector<Element> welded(uniqueCount);
++ QSSGMesh::remapVertexBuffer(welded.data(), data.constData(), vertexCount, sizeof(Element), remap.constData());
++ data = std::move(welded);
++ };
++ remapStream(primitive.positions);
++ remapStream(primitive.normals);
++ remapStream(primitive.tangents);
++ remapStream(primitive.binormals);
++ remapStream(primitive.uv0);
++ remapStream(primitive.uv1);
++ remapStream(primitive.colors);
++ remapStream(primitive.joints);
++ remapStream(primitive.weights);
++ for (MorphTargetData &target : primitive.targets) {
++ remapStream(target.positions);
++ remapStream(target.normals);
++ remapStream(target.tangents);
++ }
++
++ QSSGMesh::remapIndexBuffer(primitive.indexes.data(), primitive.indexes.constData(),
++ primitive.indexes.size(), remap.constData());
++ }
++
++ bool loadPrimitive(const QSSGGltfDocument &document, const MeshPrimitive &source,
++ const GltfSceneConverter::Options &options, PrimitiveData &primitive)
++ {
++ const int positionAccessor = source.attributes.value(QByteArrayLiteral("POSITION"), -1);
++ if (positionAccessor < 0) {
++ qCWarning(lcQuick3DGltf) << "Skipping primitive without POSITION data";
++ return false;
++ }
++
++ primitive.positions = toVectors<QVector3D>(QSSGGltfAccessorReader::readAsFloats(document, positionAccessor), 3);
++ const qsizetype vertexCount = primitive.positions.size();
++
++ // Indices; non-indexed primitives get a sequential index buffer
++ QVector<quint32> indexes;
++ if (source.indices >= 0) {
++ indexes = QVector<quint32>(QSSGGltfAccessorReader::readIndices(document, source.indices));
++ } else {
++ indexes.resize(vertexCount);
++ std::iota(indexes.begin(), indexes.end(), 0);
++ }
++ primitive.indexes = toTriangleList(source.mode, std::move(indexes));
++ if (primitive.indexes.isEmpty()) {
++ if (source.mode != MeshPrimitive::Triangles && source.mode != MeshPrimitive::TriangleStrip
++ && source.mode != MeshPrimitive::TriangleFan) {
++ qCWarning(lcQuick3DGltf) << "Skipping primitive with unsupported mode" << source.mode
++ << "(points and lines are not supported)";
++ }
++ return false;
++ }
++
++ // Everything below indexes the vertex data directly
++ for (const quint32 index : std::as_const(primitive.indexes)) {
++ if (qsizetype(index) >= vertexCount) {
++ qCWarning(lcQuick3DGltf) << "Skipping primitive with out-of-range index value" << index;
++ return false;
++ }
++ }
++
++ // TANGENT needs the normals, so NORMAL comes first
++ static const QByteArray knownSemantics[] = {
++ QByteArrayLiteral("POSITION"), QByteArrayLiteral("NORMAL"), QByteArrayLiteral("TANGENT"),
++ QByteArrayLiteral("TEXCOORD_0"), QByteArrayLiteral("TEXCOORD_1"), QByteArrayLiteral("COLOR_0"),
++ QByteArrayLiteral("JOINTS_0"), QByteArrayLiteral("WEIGHTS_0"),
++ };
++ for (auto it = source.attributes.constBegin(); it != source.attributes.constEnd(); ++it) {
++ if (std::find(std::begin(knownSemantics), std::end(knownSemantics), it.key()) == std::end(knownSemantics)) {
++ qCWarning(lcQuick3DGltf) << "Ignoring unsupported vertex attribute" << it.key();
++ }
++ }
++
++ for (const QByteArray &semantic : knownSemantics) {
++ const int accessorIndex = source.attributes.value(semantic, -1);
++ if (accessorIndex < 0 || accessorIndex >= document.accessors.size())
++ continue;
++ const Accessor &accessor = document.accessors.at(accessorIndex);
++ const auto floats = [&] { return QSSGGltfAccessorReader::readAsFloats(document, accessorIndex); };
++
++ if (semantic == QByteArrayLiteral("POSITION")) {
++ // already read
++ } else if (semantic == QByteArrayLiteral("NORMAL")) {
++ primitive.normals = toVectors<QVector3D>(floats(), 3);
++ } else if (semantic == QByteArrayLiteral("TANGENT")) {
++ // The specification requires ignoring tangents without normals
++ if (primitive.normals.isEmpty())
++ continue;
++ // xyz tangent, w handedness
++ const QList<float> data = floats();
++ const qsizetype count = data.size() / 4;
++ primitive.tangents.reserve(count);
++ primitive.binormals.reserve(count);
++ for (qsizetype i = 0; i < count; ++i) {
++ const QVector3D tangent(data[i * 4], data[i * 4 + 1], data[i * 4 + 2]);
++ const float handedness = data[i * 4 + 3];
++ primitive.tangents.append(tangent);
++ const QVector3D normal = primitive.normals.value(i);
++ primitive.binormals.append(QVector3D::crossProduct(normal, tangent) * handedness);
++ }
++ } else if (semantic == QByteArrayLiteral("TEXCOORD_0") || semantic == QByteArrayLiteral("TEXCOORD_1")) {
++ // glTF UV origin is top-left, Qt Quick 3D bottom-left
++ auto uv = toVectors<QVector2D>(floats(), Accessor::componentCount(accessor.type));
++ for (QVector2D &coord : uv)
++ coord.setY(1.0f - coord.y());
++ if (semantic == QByteArrayLiteral("TEXCOORD_0"))
++ primitive.uv0 = std::move(uv);
++ else
++ primitive.uv1 = std::move(uv);
++ } else if (semantic == QByteArrayLiteral("COLOR_0")) {
++ const int components = Accessor::componentCount(accessor.type);
++ if (components == 3) {
++ const QList<float> data = floats();
++ const qsizetype count = data.size() / 3;
++ primitive.colors.reserve(count);
++ for (qsizetype i = 0; i < count; ++i)
++ primitive.colors.append(QVector4D(data[i * 3], data[i * 3 + 1], data[i * 3 + 2], 1.0f));
++ } else {
++ primitive.colors = toVectors<QVector4D>(floats(), 4);
++ }
++ } else if (semantic == QByteArrayLiteral("JOINTS_0")) {
++ const QList<float> data = floats();
++ const qsizetype count = data.size() / 4;
++ primitive.joints.reserve(count);
++ // Also maps NaN, whose cast would be undefined, to 0
++ const auto toJointIndex = [](float value) {
++ return value >= 0.0f && value < 2.0e9f ? qint32(value) : 0;
++ };
++ for (qsizetype i = 0; i < count; ++i) {
++ primitive.joints.append({ toJointIndex(data[i * 4]), toJointIndex(data[i * 4 + 1]),
++ toJointIndex(data[i * 4 + 2]), toJointIndex(data[i * 4 + 3]) });
++ }
++ } else if (semantic == QByteArrayLiteral("WEIGHTS_0")) {
++ auto weights = toVectors<QVector4D>(floats(), 4);
++ // Renormalize so the four influences sum to one
++ for (QVector4D &weight : weights) {
++ const float sum = weight.x() + weight.y() + weight.z() + weight.w();
++ if (!qFuzzyIsNull(sum) && !qFuzzyCompare(sum, 1.0f))
++ weight /= sum;
++ }
++ primitive.weights = std::move(weights);
++ }
++ }
++
++ // The renderer supports at most 8 morph targets
++ constexpr int maxMorphTargets = 8;
++ if (source.targets.size() > maxMorphTargets) {
++ qCWarning(lcQuick3DGltf) << "Mesh primitive has" << source.targets.size()
++ << "morph targets, only the first" << maxMorphTargets << "are used";
++ }
++ const qsizetype targetCount = qMin(qsizetype(maxMorphTargets), source.targets.size());
++ for (qsizetype targetIndex = 0; targetIndex < targetCount; ++targetIndex) {
++ const QHash<QByteArray, int> &sourceTarget = source.targets.at(targetIndex);
++ MorphTargetData target;
++ if (const int accessor = sourceTarget.value(QByteArrayLiteral("POSITION"), -1); accessor >= 0)
++ target.positions = toVectors<QVector3D>(QSSGGltfAccessorReader::readAsFloats(document, accessor), 3);
++ if (const int accessor = sourceTarget.value(QByteArrayLiteral("NORMAL"), -1); accessor >= 0)
++ target.normals = toVectors<QVector3D>(QSSGGltfAccessorReader::readAsFloats(document, accessor), 3);
++ if (const int accessor = sourceTarget.value(QByteArrayLiteral("TANGENT"), -1); accessor >= 0)
++ target.tangents = toVectors<QVector3D>(QSSGGltfAccessorReader::readAsFloats(document, accessor), 3);
++ primitive.targets.append(target);
++ }
++
++ // Everything below relies on matching attribute counts
++ const auto normalizeCount = [vertexCount](auto &data, const char *what) {
++ if (!data.isEmpty() && data.size() != vertexCount) {
++ qCWarning(lcQuick3DGltf) << what << "data has" << data.size() << "elements, expected" << vertexCount;
++ data.resize(vertexCount);
++ }
++ };
++ normalizeCount(primitive.normals, "NORMAL");
++ normalizeCount(primitive.tangents, "TANGENT");
++ normalizeCount(primitive.binormals, "TANGENT");
++ normalizeCount(primitive.uv0, "TEXCOORD_0");
++ normalizeCount(primitive.uv1, "TEXCOORD_1");
++ normalizeCount(primitive.colors, "COLOR_0");
++ normalizeCount(primitive.joints, "JOINTS_0");
++ normalizeCount(primitive.weights, "WEIGHTS_0");
++ for (MorphTargetData &target : primitive.targets) {
++ normalizeCount(target.positions, "morph target POSITION");
++ normalizeCount(target.normals, "morph target NORMAL");
++ normalizeCount(target.tangents, "morph target TANGENT");
++ }
++
++ if (primitive.normals.isEmpty())
++ generateNormals(primitive, options.generateSmoothNormals);
++
++ // Switchable variants need tangents if any of their materials does
++ const auto anyVariantNeedsTangents = [&] {
++ if (!options.materialVariant.isEmpty() || document.materialVariants.isEmpty())
++ return false;
++ for (const MeshPrimitive::VariantMapping &mapping : source.variantMappings) {
++ if (materialNeedsTangents(document, mapping.material))
++ return true;
++ }
++ return false;
++ };
++ if (primitive.tangents.isEmpty()
++ && (options.forceTangentGeneration
++ || materialNeedsTangents(document, source.effectiveMaterial(options.materialVariantIndex))
++ || anyVariantNeedsTangents())) {
++ generateTangents(primitive);
++ }
++
++ // glTF targets are deltas, the renderer expects absolute values
++ for (MorphTargetData &target : primitive.targets) {
++ for (qsizetype i = 0; i < target.positions.size() && i < primitive.positions.size(); ++i)
++ target.positions[i] += primitive.positions.at(i);
++ for (qsizetype i = 0; i < target.normals.size() && i < primitive.normals.size(); ++i)
++ target.normals[i] += primitive.normals.at(i);
++ for (qsizetype i = 0; i < target.tangents.size() && i < primitive.tangents.size(); ++i)
++ target.tangents[i] += primitive.tangents.at(i);
++ }
++
++ if (options.joinIdenticalVertices)
++ weldVertices(primitive);
++
++ if (const int materialIndex = source.effectiveMaterial(options.materialVariantIndex); materialIndex >= 0)
++ primitive.materialName = document.materials.at(materialIndex).name;
++
++ QSSGMesh::optimizeVertexCache(primitive.indexes.data(), primitive.indexes.data(),
++ primitive.indexes.size(), primitive.positions.size());
++ return true;
++ }
++
++ template<typename T>
++ void appendVertexData(QByteArray &buffer, const QVector<T> &data, qsizetype vertexCount)
++ {
++ if (!data.isEmpty())
++ buffer.append(reinterpret_cast<const char *>(data.constData()), data.size() * sizeof(T));
++ // Zero-fill so all attributes cover the same vertex range
++ if (data.size() < vertexCount)
++ buffer.append((vertexCount - data.size()) * sizeof(T), '\0');
++ }
++
++ } // namespace
++
++ /*!
++ \internal
++
++ Builds a QSSGMesh::Mesh from the glTF mesh \a mesh in \a document, with
++ one subset per triangle primitive. Primitives with non-triangle topology
++ that cannot be converted (points and lines) are skipped with a warning.
++ Returns an invalid mesh when no primitive could be converted. When
++ \a usedPrimitives is given, it receives the indices of the source
++ primitives that became subsets, in subset order, so callers can build a
++ matching material list. \a morphInfo receives the number of morph targets
++ stored in the mesh and which attributes they carry.
++ */
++ QSSGMesh::Mesh buildMesh(const QSSGGltfDocument &document, const QSSGGltf::Mesh &mesh,
++ const GltfSceneConverter::Options &options,
++ QList<int> *usedPrimitives,
++ GltfMorphTargetInfo *morphInfo)
++ {
++ QVector<PrimitiveData> primitives;
++ primitives.reserve(mesh.primitives.size());
++ Requirements requirements;
++ for (qsizetype primitiveIndex = 0; primitiveIndex < mesh.primitives.size(); ++primitiveIndex) {
++ PrimitiveData primitive;
++ if (loadPrimitive(document, mesh.primitives.at(primitiveIndex), options, primitive)) {
++ requirements.collect(primitive);
++ primitives.append(std::move(primitive));
++ if (usedPrimitives)
++ usedPrimitives->append(int(primitiveIndex));
++ }
++ }
++
++ if (primitives.isEmpty())
++ return {};
++
++ QByteArray indexBufferData;
++ QByteArray positionData;
++ QByteArray normalData;
++ QByteArray tangentData;
++ QByteArray binormalData;
++ QByteArray uv0Data;
++ QByteArray uv1Data;
++ QByteArray colorData;
++ QByteArray jointData;
++ QByteArray weightData;
++ // vec3 padded to vec4, one buffer per target and component
++ struct TargetBuffers {
++ QByteArray positions;
++ QByteArray normals;
++ QByteArray tangents;
++ };
++ QVector<TargetBuffers> targetData(requirements.numMorphTargets);
++ const auto appendPaddedVec3 = [](QByteArray &buffer, const QVector<QVector3D> &data, qsizetype vertexCount) {
++ for (const QVector3D &v : data) {
++ const float padded[4] = { v.x(), v.y(), v.z(), 0.0f };
++ buffer.append(reinterpret_cast<const char *>(padded), sizeof(padded));
++ }
++ if (data.size() < vertexCount)
++ buffer.append((vertexCount - data.size()) * 4 * sizeof(float), '\0');
++ };
++ QVector<QSSGMesh::AssetMeshSubset> subsets;
++
++ // 32-bit, since Metal needs 4 byte aligned index buffer offsets
++ const auto indexType = QSSGMesh::Mesh::ComponentType::UnsignedInt32;
++
++ const auto appendCopy = [](auto &data, qsizetype sourceIndex) {
++ if (!data.isEmpty())
++ data.append(data.at(sourceIndex));
++ };
++
++ quint32 baseVertex = 0;
++ for (PrimitiveData &primitive : primitives) {
++ quint32 indexOffset = indexBufferData.size() / QSSGMesh::MeshInternal::byteSizeForComponentType(indexType);
++
++ // LOD indexes precede the original ones, lowest quality first
++ QVector<QSSGMesh::Mesh::Lod> meshLods;
++ QVector<quint32> lodIndexes;
++ if (options.generateMeshLODs && !primitive.normals.isEmpty()) {
++ QVector<QSSGMesh::MeshVertexSplit> splitVertices;
++ const auto lods = QSSGMesh::generateMeshLevelsOfDetail(primitive.positions, primitive.normals,
++ primitive.indexes, splitVertices,
++ options.lodNormalMergeAngle,
++ options.lodNormalSplitAngle);
++ // Split vertices are copies with a recalculated normal
++ for (const QSSGMesh::MeshVertexSplit &split : std::as_const(splitVertices)) {
++ primitive.positions.append(primitive.positions.at(split.sourceIndex));
++ primitive.normals.append(split.normal);
++ appendCopy(primitive.tangents, split.sourceIndex);
++ appendCopy(primitive.binormals, split.sourceIndex);
++ appendCopy(primitive.uv0, split.sourceIndex);
++ appendCopy(primitive.uv1, split.sourceIndex);
++ appendCopy(primitive.colors, split.sourceIndex);
++ appendCopy(primitive.joints, split.sourceIndex);
++ appendCopy(primitive.weights, split.sourceIndex);
++ for (MorphTargetData &target : primitive.targets) {
++ appendCopy(target.positions, split.sourceIndex);
++ appendCopy(target.normals, split.sourceIndex);
++ appendCopy(target.tangents, split.sourceIndex);
++ }
++ }
++ for (const QSSGMesh::MeshLevelOfDetail &lodEntry : lods) {
++ QSSGMesh::Mesh::Lod lod;
++ lod.offset = indexOffset;
++ lod.count = lodEntry.indexes.size();
++ lod.distance = lodEntry.distance;
++ meshLods.prepend(lod);
++ indexOffset += lod.count;
++ QVector<quint32> currentLodIndexes = lodEntry.indexes;
++ QSSGMesh::optimizeVertexCache(currentLodIndexes.data(), currentLodIndexes.data(),
++ currentLodIndexes.size(), primitive.positions.size());
++ lodIndexes += currentLodIndexes;
++ }
++ }
++ const qsizetype vertexCount = primitive.positions.size();
++
++ QVector<quint32> globalIndexes = lodIndexes + primitive.indexes;
++ primitive.indexes.clear();
++ for (quint32 &index : globalIndexes)
++ index += baseVertex;
++ indexBufferData.append(reinterpret_cast<const char *>(globalIndexes.constData()),
++ globalIndexes.size() * sizeof(quint32));
++ const quint32 subsetIndexCount = globalIndexes.size() - lodIndexes.size();
++
++ appendVertexData(positionData, primitive.positions, vertexCount);
++ if (requirements.needsNormals)
++ appendVertexData(normalData, primitive.normals, vertexCount);
++ if (requirements.needsTangents) {
++ appendVertexData(tangentData, primitive.tangents, vertexCount);
++ appendVertexData(binormalData, primitive.binormals, vertexCount);
++ }
++ if (requirements.needsUV0)
++ appendVertexData(uv0Data, primitive.uv0, vertexCount);
++ if (requirements.needsUV1)
++ appendVertexData(uv1Data, primitive.uv1, vertexCount);
++ if (requirements.needsColors)
++ appendVertexData(colorData, primitive.colors, vertexCount);
++ if (requirements.needsJoints) {
++ if (options.useFloatJointIndices) {
++ QVector<QVector4D> floatJoints;
++ floatJoints.reserve(primitive.joints.size());
++ for (const IntVector4D &joint : std::as_const(primitive.joints))
++ floatJoints.append(QVector4D(float(joint.x), float(joint.y), float(joint.z), float(joint.w)));
++ appendVertexData(jointData, floatJoints, vertexCount);
++ } else {
++ appendVertexData(jointData, primitive.joints, vertexCount);
++ }
++ appendVertexData(weightData, primitive.weights, vertexCount);
++ }
++
++ for (int targetIndex = 0; targetIndex < requirements.numMorphTargets; ++targetIndex) {
++ const MorphTargetData target = primitive.targets.value(targetIndex);
++ if (requirements.needsTargetPositions)
++ appendPaddedVec3(targetData[targetIndex].positions, target.positions, vertexCount);
++ if (requirements.needsTargetNormals)
++ appendPaddedVec3(targetData[targetIndex].normals, target.normals, vertexCount);
++ if (requirements.needsTargetTangents)
++ appendPaddedVec3(targetData[targetIndex].tangents, target.tangents, vertexCount);
++ }
++
++ subsets.append({ primitive.materialName,
++ subsetIndexCount,
++ indexOffset,
++ 0, // the builder calculates the bounds from the position data
++ 0, 0, // lightmap size hint
++ meshLods });
++ baseVertex += vertexCount;
++ }
++
++ QVector<QSSGMesh::AssetVertexEntry> entries;
++ entries.append({ QSSGMesh::MeshInternal::getPositionAttrName(), positionData,
++ QSSGMesh::Mesh::ComponentType::Float32, 3 });
++ if (!normalData.isEmpty()) {
++ entries.append({ QSSGMesh::MeshInternal::getNormalAttrName(), normalData,
++ QSSGMesh::Mesh::ComponentType::Float32, 3 });
++ }
++ if (!uv0Data.isEmpty()) {
++ entries.append({ QSSGMesh::MeshInternal::getUV0AttrName(), uv0Data,
++ QSSGMesh::Mesh::ComponentType::Float32, 2 });
++ }
++ if (!uv1Data.isEmpty()) {
++ entries.append({ QSSGMesh::MeshInternal::getUV1AttrName(), uv1Data,
++ QSSGMesh::Mesh::ComponentType::Float32, 2 });
++ }
++ if (!tangentData.isEmpty()) {
++ entries.append({ QSSGMesh::MeshInternal::getTexTanAttrName(), tangentData,
++ QSSGMesh::Mesh::ComponentType::Float32, 3 });
++ entries.append({ QSSGMesh::MeshInternal::getTexBinormalAttrName(), binormalData,
++ QSSGMesh::Mesh::ComponentType::Float32, 3 });
++ }
++ if (!colorData.isEmpty()) {
++ entries.append({ QSSGMesh::MeshInternal::getColorAttrName(), colorData,
++ QSSGMesh::Mesh::ComponentType::Float32, 4 });
++ }
++ if (!jointData.isEmpty()) {
++ entries.append({ QSSGMesh::MeshInternal::getJointAttrName(), jointData,
++ options.useFloatJointIndices ? QSSGMesh::Mesh::ComponentType::Float32
++ : QSSGMesh::Mesh::ComponentType::Int32, 4 });
++ entries.append({ QSSGMesh::MeshInternal::getWeightAttrName(), weightData,
++ QSSGMesh::Mesh::ComponentType::Float32, 4 });
++ }
++
++ // Grouped per target, as Mesh::fromAssetData expects
++ int numTargetComps = 0;
++ if (requirements.numMorphTargets > 0) {
++ numTargetComps = int(requirements.needsTargetPositions) + int(requirements.needsTargetNormals)
++ + int(requirements.needsTargetTangents);
++ for (int targetIndex = 0; targetIndex < requirements.numMorphTargets; ++targetIndex) {
++ if (requirements.needsTargetPositions) {
++ entries.append({ QSSGMesh::MeshInternal::getPositionAttrName(), targetData[targetIndex].positions,
++ QSSGMesh::Mesh::ComponentType::Float32, 3, targetIndex });
++ }
++ if (requirements.needsTargetNormals) {
++ entries.append({ QSSGMesh::MeshInternal::getNormalAttrName(), targetData[targetIndex].normals,
++ QSSGMesh::Mesh::ComponentType::Float32, 3, targetIndex });
++ }
++ if (requirements.needsTargetTangents) {
++ entries.append({ QSSGMesh::MeshInternal::getTexTanAttrName(), targetData[targetIndex].tangents,
++ QSSGMesh::Mesh::ComponentType::Float32, 3, targetIndex });
++ }
++ }
++ }
++
++ if (morphInfo) {
++ morphInfo->count = requirements.numMorphTargets;
++ morphInfo->hasPositions = requirements.needsTargetPositions;
++ morphInfo->hasNormals = requirements.needsTargetNormals;
++ morphInfo->hasTangents = requirements.needsTargetTangents;
++ }
++
++ return QSSGMesh::Mesh::fromAssetData(entries, indexBufferData, indexType, subsets,
++ quint32(requirements.numMorphTargets), quint32(numTargetComps));
++ }
++
++ } // namespace GltfMeshBuilder
++
++ QT_END_NAMESPACE