Revision control
++ // Copyright (C) 2019 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
++ // Qt-Security score:critical reason:data-parser
++
++
++ #include "qssgmesh_p.h"
++
++ #include <QtCore/QVector>
++ #include <QtCore/qhash.h>
++ #include <QtCore/qvarlengtharray.h>
++ #include <QtCore/qmath.h>
++ #include <QtQuick3DUtils/private/qssgdataref_p.h>
++ #include <QtQuick3DUtils/private/qssglightmapuvgenerator_p.h>
++ #include <QtQuick3DUtils/private/qssgutils_p.h>
++
++ #include "meshoptimizer.h"
++
++ #include <algorithm>
++
++ QT_BEGIN_NAMESPACE
++
++ namespace QSSGMesh {
++
++ // fileId, fileVersion, offset, count
++ static const size_t MULTI_HEADER_STRUCT_SIZE = 16;
++
++ // meshOffset, meshId, padding
++ static const size_t MULTI_ENTRY_STRUCT_SIZE = 16;
++
++ // fileId, fileVersion, flags, size
++ static const size_t MESH_HEADER_STRUCT_SIZE = 12;
++
++ // vertexBuffer, indexBuffer, subsets, joints, drawMode, winding
++ static const size_t MESH_STRUCT_SIZE = 56;
++
++ // vertex buffer entry list: nameOffset, componentType, componentCount, offset
++ static const size_t VERTEX_BUFFER_ENTRY_STRUCT_SIZE = 16;
++
++ // subset list: count, offset, minXYZ, maxXYZ, nameOffset, nameLength
++ static const size_t SUBSET_STRUCT_SIZE_V3_V4 = 40;
++ // subset list: count, offset, minXYZ, maxXYZ, nameOffset, nameLength, lightmapSizeWidth, lightmapSizeHeight
++ static const size_t SUBSET_STRUCT_SIZE_V5 = 48;
++ // subset list: count, offset, minXYZ, maxXYZ, nameOffset, nameLength, lightmapSizeWidth, lightmapSizeHeight, lodCount
++ static const size_t SUBSET_STRUCT_SIZE_V6 = 52;
++
++ //lod entry: count, offset, distance
++ static const size_t LOD_STRUCT_SIZE = 12;
++
++ // joint list: jointID, parentID, invBindPose, localToGlobalBoneSpace
++ static const size_t JOINT_STRUCT_SIZE = 136;
++
++ // getSizeOfType() is Q_UNREACHABLE outside its enumeration, so a component type
++ // from the file has to be checked before it gets there.
++ // Spelled out so that a new component type fails to compile until handled.
++ static bool isKnownComponentType(quint32 value)
++ {
++ switch (Mesh::ComponentType(value)) {
++ case Mesh::ComponentType::UnsignedInt8:
++ case Mesh::ComponentType::Int8:
++ case Mesh::ComponentType::UnsignedInt16:
++ case Mesh::ComponentType::Int16:
++ case Mesh::ComponentType::UnsignedInt32:
++ case Mesh::ComponentType::Int32:
++ case Mesh::ComponentType::UnsignedInt64:
++ case Mesh::ComponentType::Int64:
++ case Mesh::ComponentType::Float16:
++ case Mesh::ComponentType::Float32:
++ case Mesh::ComponentType::Float64:
++ return true;
++ }
++ return false;
++ }
++
++ // Bounds a count by the file rather than by a chosen cap, so the limit scales
++ // with the file and cannot be set wrong.
++ static bool canRead(QIODevice *device, quint64 count, quint64 itemSize)
++ {
++ quint64 bytes = 0;
++ if (qMulOverflow(count, itemSize, &bytes))
++ return false;
++ const qint64 remaining = device->size() - device->pos();
++ return remaining >= 0 && bytes <= quint64(remaining);
++ }
++
++ MeshInternal::MultiMeshInfo MeshInternal::readFileHeader(QIODevice *device)
++ {
++ const qint64 multiHeaderStartOffset = device->size() - qint64(MULTI_HEADER_STRUCT_SIZE);
++ if (multiHeaderStartOffset < 0) {
++ qWarning("Mesh file is too small to hold a header");
++ return {};
++ }
++
++ device->seek(multiHeaderStartOffset);
++ QDataStream inputStream(device);
++ inputStream.setByteOrder(QDataStream::LittleEndian);
++ inputStream.setFloatingPointPrecision(QDataStream::SinglePrecision);
++
++ MultiMeshInfo meshFileInfo;
++ inputStream >> meshFileInfo.fileId >> meshFileInfo.fileVersion;
++
++ if (!meshFileInfo.isValid()) {
++ qWarning("Mesh file invalid");
++ return {};
++ }
++
++ quint32 multiEntriesOffset; // unused, the entry list is right before the header
++ quint32 meshCount;
++ inputStream >> multiEntriesOffset >> meshCount;
++
++ // The entry list sits immediately before the header, so it bounds the count.
++ if (quint64(meshCount) * MULTI_ENTRY_STRUCT_SIZE > quint64(multiHeaderStartOffset)) {
++ qWarning("Mesh file declares %u meshes, which do not fit in it", meshCount);
++ return {};
++ }
++
++ for (quint32 i = 0; i < meshCount; ++i) {
++ device->seek(multiHeaderStartOffset
++ - (qint64(MULTI_ENTRY_STRUCT_SIZE) * meshCount)
++ + (qint64(MULTI_ENTRY_STRUCT_SIZE) * i));
++ quint64 offset;
++ quint32 id;
++ inputStream >> offset >> id;
++ meshFileInfo.meshEntries.insert(id, offset);
++ }
++
++ return meshFileInfo;
++ }
++
++ void MeshInternal::writeFileHeader(QIODevice *device, const MeshInternal::MultiMeshInfo &meshFileInfo)
++ {
++ QDataStream outputStream(device);
++ outputStream.setByteOrder(QDataStream::LittleEndian);
++ outputStream.setFloatingPointPrecision(QDataStream::SinglePrecision);
++
++ const quint32 multiEntriesOffset = device->pos();
++ for (auto it = meshFileInfo.meshEntries.cbegin(), end = meshFileInfo.meshEntries.cend(); it != end; ++it) {
++ const quint32 id = it.key();
++ const quint64 offset = it.value();
++ const quint32 padding = 0;
++ outputStream << offset << id << padding;
++ }
++
++ const quint32 meshCount = meshFileInfo.meshEntries.size();
++ outputStream << meshFileInfo.fileId << meshFileInfo.fileVersion << multiEntriesOffset << meshCount;
++ }
++
++ quint64 MeshInternal::readMeshData(QIODevice *device, quint64 offset, Mesh *mesh, MeshDataHeader *header)
++ {
++ static char alignPadding[4] = {};
++
++ device->seek(offset);
++ QDataStream inputStream(device);
++ inputStream.setByteOrder(QDataStream::LittleEndian);
++ inputStream.setFloatingPointPrecision(QDataStream::SinglePrecision);
++
++ inputStream >> header->fileId >> header->fileVersion >> header->flags >> header->sizeInBytes;
++ if (!header->isValid()) {
++ qWarning() << "Mesh data invalid";
++ if (header->fileId == MeshDataHeader::FILE_ID) {
++ if (header->fileVersion > MeshDataHeader::FILE_VERSION)
++ qWarning() << "File version " << header->fileVersion << " newer than " << MeshDataHeader::FILE_VERSION;
++ if (header->fileVersion < MeshDataHeader::LEGACY_MESH_FILE_VERSION)
++ qWarning() << "File version " << header->fileVersion << " older than " << MeshDataHeader::LEGACY_MESH_FILE_VERSION;
++ } else {
++ qWarning() << "Invalid file ID" << header->fileId;
++ }
++ return 0;
++ }
++
++ MeshInternal::MeshOffsetTracker offsetTracker(offset + MESH_HEADER_STRUCT_SIZE);
++ Q_ASSERT(offsetTracker.offset() == device->pos());
++
++ quint32 targetBufferEntriesCount;
++ quint32 vertexBufferEntriesCount;
++ quint32 targetBufferDataSize;
++ quint32 vertexBufferDataSize;
++ inputStream >> targetBufferEntriesCount
++ >> vertexBufferEntriesCount
++ >> mesh->m_vertexBuffer.stride
++ >> targetBufferDataSize
++ >> vertexBufferDataSize;
++
++ if (!header->hasSeparateTargetBuffer()) {
++ targetBufferEntriesCount = 0;
++ targetBufferDataSize = 0;
++ }
++
++ quint32 indexBufferComponentType;
++ quint32 indexBufferDataOffset;
++ quint32 indexBufferDataSize;
++ inputStream >> indexBufferComponentType
++ >> indexBufferDataOffset
++ >> indexBufferDataSize;
++ if (!isKnownComponentType(indexBufferComponentType)) {
++ qWarning("Mesh index buffer has unknown component type %u", indexBufferComponentType);
++ return 0;
++ }
++ mesh->m_indexBuffer.componentType = Mesh::ComponentType(indexBufferComponentType);
++
++ quint32 targetCount;
++ quint32 subsetsCount;
++ inputStream >> targetCount >> subsetsCount;
++ // Before version 7 this field is a leftover subset offset, not a count;
++ // the legacy path below works the count out from the attribute names.
++ if (header->hasSeparateTargetBuffer())
++ mesh->m_targetBuffer.numTargets = targetCount;
++
++ quint32 jointsOffsets; // unused, see the format documentation
++ quint32 jointsCount;
++ inputStream >> jointsOffsets >> jointsCount;
++ quint32 drawMode;
++ quint32 winding;
++ inputStream >> drawMode >> winding;
++ mesh->m_drawMode = Mesh::DrawMode(drawMode);
++ mesh->m_winding = Mesh::Winding(winding);
++
++ offsetTracker.advance(MESH_STRUCT_SIZE);
++
++ if (!canRead(device, vertexBufferEntriesCount, VERTEX_BUFFER_ENTRY_STRUCT_SIZE)) {
++ qWarning("Mesh declares %u vertex buffer entries, which do not fit in the file",
++ vertexBufferEntriesCount);
++ return 0;
++ }
++
++ quint32 entriesByteSize = 0;
++ for (quint32 i = 0; i < vertexBufferEntriesCount; ++i) {
++ Mesh::VertexBufferEntry vertexBufferEntry;
++ quint32 componentType;
++ quint32 nameOffset; // unused
++ inputStream >> nameOffset
++ >> componentType
++ >> vertexBufferEntry.componentCount
++ >> vertexBufferEntry.offset;
++ if (!isKnownComponentType(componentType)) {
++ qWarning("Mesh attribute %u has unknown component type %u", i, componentType);
++ return 0;
++ }
++ vertexBufferEntry.componentType = Mesh::ComponentType(componentType);
++ mesh->m_vertexBuffer.entries.append(vertexBufferEntry);
++ entriesByteSize += VERTEX_BUFFER_ENTRY_STRUCT_SIZE;
++ }
++ quint32 alignAmount = offsetTracker.alignedAdvance(entriesByteSize);
++ if (alignAmount)
++ device->read(alignPadding, alignAmount);
++
++ // vertex buffer entry names
++ quint32 numTargets = 0;
++ // used for recording the target attributes supported by the mesh
++ // and re-construting it when meeting attr_unsupported
++ QList<QByteArray> attrNames;
++ for (auto &entry : mesh->m_vertexBuffer.entries) {
++ quint32 nameLength;
++ inputStream >> nameLength;
++ offsetTracker.advance(sizeof(quint32));
++ if (!canRead(device, nameLength, 1)) {
++ qWarning("Mesh declares a %u byte attribute name, which does not fit in the file",
++ nameLength);
++ return 0;
++ }
++ const QByteArray nameWithZeroTerminator = device->read(nameLength);
++ entry.name = QByteArray(nameWithZeroTerminator.constData(), qMax(0, nameWithZeroTerminator.size() - 1));
++ alignAmount = offsetTracker.alignedAdvance(nameLength);
++ if (alignAmount)
++ device->read(alignPadding, alignAmount);
++ // Old morph meshes' target attributes were appended sequentially
++ // behind vertex attributes. However, since the number of targets are restricted by 8
++ // the other attributes were named by "attr_unsupported"
++ // So just checking numTargets is safe with the above assumption and
++ // it will try to reconstruct the unsupported attributes.
++ if (numTargets > 0 || (!header->hasSeparateTargetBuffer() && entry.name.startsWith("attr_t"))) {
++ // Any later name lands here once a target has been seen, including
++ // short ones, and sliced() asserts where mid() would not.
++ const QByteArray suffix = entry.name.size() > 6 ? entry.name.sliced(6) : QByteArray();
++ if (suffix.startsWith("pos")) {
++ const quint32 targetId = entry.name.mid(9).toUInt();
++ // All the attributes of the first target should be recorded correctly.
++ if (targetId == 0)
++ attrNames.append(MeshInternal::getPositionAttrName());
++ numTargets = qMax(numTargets, targetId + 1);
++ entry.name = MeshInternal::getPositionAttrName();
++ mesh->m_targetBuffer.entries.append(entry);
++ targetBufferEntriesCount++;
++ } else if (suffix.startsWith("norm")) {
++ const quint32 targetId = entry.name.mid(10).toUInt();
++ if (targetId == 0)
++ attrNames.append(MeshInternal::getNormalAttrName());
++ numTargets = qMax(numTargets, targetId + 1);
++ entry.name = MeshInternal::getNormalAttrName();
++ mesh->m_targetBuffer.entries.append(entry);
++ targetBufferEntriesCount++;
++ } else if (suffix.startsWith("tan")) {
++ const quint32 targetId = entry.name.mid(9).toUInt();
++ if (targetId == 0)
++ attrNames.append(MeshInternal::getTexTanAttrName());
++ numTargets = qMax(numTargets, targetId + 1);
++ entry.name = MeshInternal::getTexTanAttrName();
++ mesh->m_targetBuffer.entries.append(entry);
++ targetBufferEntriesCount++;
++ } else if (suffix.startsWith("binorm")) {
++ const quint32 targetId = entry.name.mid(12).toUInt();
++ if (targetId == 0)
++ attrNames.append(MeshInternal::getTexBinormalAttrName());
++ numTargets = qMax(numTargets, targetId + 1);
++ entry.name = MeshInternal::getTexBinormalAttrName();
++ mesh->m_targetBuffer.entries.append(entry);
++ targetBufferEntriesCount++;
++ } else if (entry.name.startsWith("attr_unsupported")) {
++ // Reconstruct, if target 0 recorded a layout to do it from.
++ if (attrNames.isEmpty()) {
++ qWarning("Mesh has an unsupported morph target attribute with no target 0 to "
++ "take the layout from");
++ return 0;
++ }
++ entry.name = attrNames[targetBufferEntriesCount % attrNames.size()];
++ mesh->m_targetBuffer.entries.append(entry);
++ targetBufferEntriesCount++;
++ }
++ }
++ }
++
++ if (!canRead(device, vertexBufferDataSize, 1)) {
++ qWarning("Mesh declares %u bytes of vertex data, which do not fit in the file",
++ vertexBufferDataSize);
++ return 0;
++ }
++ mesh->m_vertexBuffer.data = device->read(vertexBufferDataSize);
++ alignAmount = offsetTracker.alignedAdvance(vertexBufferDataSize);
++ if (alignAmount)
++ device->read(alignPadding, alignAmount);
++
++ if (!canRead(device, indexBufferDataSize, 1)) {
++ qWarning("Mesh declares %u bytes of index data, which do not fit in the file",
++ indexBufferDataSize);
++ return 0;
++ }
++ mesh->m_indexBuffer.data = device->read(indexBufferDataSize);
++ alignAmount = offsetTracker.alignedAdvance(indexBufferDataSize);
++ if (alignAmount)
++ device->read(alignPadding, alignAmount);
++
++ const size_t subsetStructSize = header->hasLodDataHint() ? SUBSET_STRUCT_SIZE_V6
++ : header->hasLightmapSizeHint() ? SUBSET_STRUCT_SIZE_V5
++ : SUBSET_STRUCT_SIZE_V3_V4;
++ if (!canRead(device, subsetsCount, subsetStructSize)) {
++ qWarning("Mesh declares %u subsets, which do not fit in the file", subsetsCount);
++ return 0;
++ }
++
++ quint32 subsetByteSize = 0;
++ QVector<MeshInternal::Subset> internalSubsets;
++ for (quint32 i = 0; i < subsetsCount; ++i) {
++ MeshInternal::Subset subset;
++ float minX;
++ float minY;
++ float minZ;
++ float maxX;
++ float maxY;
++ float maxZ;
++ quint32 nameOffset; // unused
++ inputStream >> subset.count
++ >> subset.offset
++ >> minX
++ >> minY
++ >> minZ
++ >> maxX
++ >> maxY
++ >> maxZ
++ >> nameOffset
++ >> subset.nameLength;
++ subset.bounds.min = QVector3D(minX, minY, minZ);
++ subset.bounds.max = QVector3D(maxX, maxY, maxZ);
++ if (header->hasLightmapSizeHint()) {
++ quint32 width = 0;
++ quint32 height = 0;
++ inputStream >> width >> height;
++ subset.lightmapSizeHint = QSize(width, height);
++ if (header->hasLodDataHint()) {
++ quint32 lodCount = 0;
++ inputStream >> lodCount;
++ subset.lodCount = lodCount;
++ subsetByteSize += SUBSET_STRUCT_SIZE_V6;
++ } else {
++ subsetByteSize += SUBSET_STRUCT_SIZE_V5;
++ }
++ } else {
++ subset.lightmapSizeHint = QSize(0, 0);
++ subsetByteSize += SUBSET_STRUCT_SIZE_V3_V4;
++ }
++ internalSubsets.append(subset);
++
++ }
++ alignAmount = offsetTracker.alignedAdvance(subsetByteSize);
++ if (alignAmount)
++ device->read(alignPadding, alignAmount);
++
++ for (MeshInternal::Subset &internalSubset : internalSubsets) {
++ if (!canRead(device, internalSubset.nameLength, 2)) {
++ qWarning("Mesh subset declares a %u character name, which does not fit in the file",
++ internalSubset.nameLength);
++ return 0;
++ }
++ internalSubset.rawNameUtf16 = device->read(qint64(internalSubset.nameLength) * 2);
++ alignAmount = offsetTracker.alignedAdvance(internalSubset.nameLength * 2);
++ if (alignAmount)
++ device->read(alignPadding, alignAmount);
++ }
++
++ quint32 lodByteSize = 0;
++ for (const MeshInternal::Subset &internalSubset : internalSubsets) {
++ // The subsets share these bytes, so check them as they are consumed.
++ if (!canRead(device, internalSubset.lodCount, LOD_STRUCT_SIZE)) {
++ qWarning("Mesh subset declares %u levels of detail, which do not fit in the file",
++ internalSubset.lodCount);
++ return 0;
++ }
++ auto meshSubset = internalSubset.toMeshSubset();
++ // Read Level of Detail data here
++ for (auto &lod : meshSubset.lods) {
++ quint32 count = 0;
++ quint32 offset = 0;
++ float distance = 0.0;
++ inputStream >> count >> offset >> distance;
++ lod.count = count;
++ lod.offset = offset;
++ lod.distance = distance;
++ lodByteSize += LOD_STRUCT_SIZE;
++ }
++
++ mesh->m_subsets.append(meshSubset);
++ }
++ alignAmount = offsetTracker.alignedAdvance(lodByteSize);
++ if (alignAmount)
++ device->read(alignPadding, alignAmount);
++
++ // Unused, but the morph target entries come after them.
++ if (jointsCount) {
++ if (!canRead(device, jointsCount, JOINT_STRUCT_SIZE)) {
++ qWarning("Mesh declares %u joints, which do not fit in the file", jointsCount);
++ return 0;
++ }
++ const quint64 jointsByteSize = quint64(jointsCount) * JOINT_STRUCT_SIZE;
++ device->seek(device->pos() + qint64(jointsByteSize));
++ offsetTracker.advance(qint32(jointsByteSize));
++ }
++
++ // Data for morphTargets
++ if (targetBufferEntriesCount > 0) {
++ if (header->hasSeparateTargetBuffer()) {
++ if (!canRead(device, targetBufferEntriesCount, VERTEX_BUFFER_ENTRY_STRUCT_SIZE)) {
++ qWarning("Mesh declares %u morph target entries, which do not fit in the file",
++ targetBufferEntriesCount);
++ return 0;
++ }
++ entriesByteSize = 0;
++ for (quint32 i = 0; i < targetBufferEntriesCount; ++i) {
++ Mesh::VertexBufferEntry targetBufferEntry;
++ quint32 componentType;
++ quint32 nameOffset; // unused
++ inputStream >> nameOffset
++ >> componentType
++ >> targetBufferEntry.componentCount
++ >> targetBufferEntry.offset;
++ if (!isKnownComponentType(componentType)) {
++ qWarning("Mesh morph target attribute %u has unknown component type %u", i,
++ componentType);
++ return 0;
++ }
++ targetBufferEntry.componentType = Mesh::ComponentType(componentType);
++ mesh->m_targetBuffer.entries.append(targetBufferEntry);
++ entriesByteSize += VERTEX_BUFFER_ENTRY_STRUCT_SIZE;
++ }
++ alignAmount = offsetTracker.alignedAdvance(entriesByteSize);
++ if (alignAmount)
++ device->read(alignPadding, alignAmount);
++
++ for (auto &entry : mesh->m_targetBuffer.entries) {
++ quint32 nameLength;
++ inputStream >> nameLength;
++ offsetTracker.advance(sizeof(quint32));
++ if (!canRead(device, nameLength, 1)) {
++ qWarning("Mesh declares a %u byte morph target name, which does not fit in "
++ "the file", nameLength);
++ return 0;
++ }
++ const QByteArray nameWithZeroTerminator = device->read(nameLength);
++ entry.name = QByteArray(nameWithZeroTerminator.constData(), qMax(0, nameWithZeroTerminator.size() - 1));
++ alignAmount = offsetTracker.alignedAdvance(nameLength);
++ if (alignAmount)
++ device->read(alignPadding, alignAmount);
++ }
++
++ if (!canRead(device, targetBufferDataSize, 1)) {
++ qWarning("Mesh declares %u bytes of morph target data, which do not fit in the "
++ "file", targetBufferDataSize);
++ return 0;
++ }
++ mesh->m_targetBuffer.data = device->read(targetBufferDataSize);
++ } else {
++ // The target buffer is sized by the product of these two, and the
++ // layout has only position, normal, tangent and binormal per target.
++ constexpr quint32 maxTargets = 8;
++ constexpr quint32 maxComponentsPerTarget = 4;
++ if (mesh->m_vertexBuffer.stride == 0 || numTargets == 0
++ || numTargets > maxTargets
++ || vertexBufferDataSize < mesh->m_vertexBuffer.stride
++ || targetBufferEntriesCount > vertexBufferEntriesCount
++ || targetBufferEntriesCount < numTargets
++ || targetBufferEntriesCount > numTargets * maxComponentsPerTarget) {
++ qWarning("Mesh morph target layout is inconsistent: stride %u, %u targets, %u "
++ "entries of %u",
++ mesh->m_vertexBuffer.stride, numTargets, targetBufferEntriesCount,
++ vertexBufferEntriesCount);
++ return 0;
++ }
++ // remove target entries from vertexbuffer entries
++ mesh->m_vertexBuffer.entries.remove(vertexBufferEntriesCount - targetBufferEntriesCount,
++ targetBufferEntriesCount);
++ // At least one vertex, so vertexCount - 1 below does not wrap.
++ const quint32 vertexCount = vertexBufferDataSize / mesh->m_vertexBuffer.stride;
++ // The least an entry can need, checked before sizing from vertexCount.
++ if (qsizetype(vertexCount - 1) * qsizetype(mesh->m_vertexBuffer.stride)
++ + 3 * qsizetype(sizeof(float))
++ > qsizetype(vertexBufferDataSize)) {
++ qWarning("Mesh morph target source does not fit a %u byte vertex buffer of stride "
++ "%u", vertexBufferDataSize, mesh->m_vertexBuffer.stride);
++ return 0;
++ }
++ const quint32 targetEntryTexWidth = qCeil(qSqrt(vertexCount));
++ qsizetype targetCompStride = 0;
++ qsizetype targetBufferSize = 0;
++ if (qMulOverflow(qsizetype(targetEntryTexWidth), qsizetype(targetEntryTexWidth),
++ &targetCompStride)
++ || qMulOverflow(targetCompStride, qsizetype(4 * sizeof(float)), &targetCompStride)
++ || qMulOverflow(targetCompStride, qsizetype(targetBufferEntriesCount),
++ &targetBufferSize)) {
++ qWarning("Mesh morph target buffer for %u vertices is too large", vertexCount);
++ return 0;
++ }
++ mesh->m_targetBuffer.data.resize(targetBufferSize);
++ const quint32 numComps = targetBufferEntriesCount / numTargets;
++ for (quint32 i = 0; i < targetBufferEntriesCount; ++i) {
++ auto &entry = mesh->m_targetBuffer.entries[i];
++ const qsizetype dstOffset = qsizetype(i / numComps) * targetCompStride
++ + qsizetype(i % numComps) * targetCompStride * numTargets;
++ // Three floats per vertex, so it is the last twelve byte copy
++ // that has to fit.
++ const qsizetype copyBytes = 3 * qsizetype(sizeof(float));
++ const qsizetype dstNeeded = dstOffset
++ + qsizetype(vertexCount - 1) * qsizetype(4 * sizeof(float)) + copyBytes;
++ const qsizetype srcNeeded = qsizetype(entry.offset)
++ + qsizetype(vertexCount - 1) * qsizetype(mesh->m_vertexBuffer.stride)
++ + copyBytes;
++ if (dstOffset < 0 || dstNeeded > targetBufferSize
++ || srcNeeded > mesh->m_vertexBuffer.data.size()) {
++ qWarning("Mesh morph target entry %u does not fit its buffer", i);
++ return 0;
++ }
++ char *dstBuf = mesh->m_targetBuffer.data.data() + dstOffset;
++ const char *srcBuf = mesh->m_vertexBuffer.data.constData() + entry.offset;
++ for (quint32 j = 0; j < vertexCount; ++j) {
++ memcpy(dstBuf + j * 4 * sizeof(float),
++ srcBuf + j * mesh->m_vertexBuffer.stride,
++ 3 * sizeof(float));
++ }
++ entry.offset = i * targetCompStride;
++ }
++ // now we don't need to have redundant targetbuffer entries
++ mesh->m_targetBuffer.entries.remove(numComps, targetBufferEntriesCount - numComps);
++ mesh->m_targetBuffer.numTargets = numTargets;
++ }
++ }
++
++ // These go straight to drawIndexed() or draw(), so a range outside the
++ // buffer it indexes has to be caught here rather than by the driver.
++ const quint32 indexSize = byteSizeForComponentType(mesh->m_indexBuffer.componentType);
++ quint64 drawableCount = 0;
++ if (!mesh->m_indexBuffer.data.isEmpty())
++ drawableCount = indexSize ? quint64(mesh->m_indexBuffer.data.size()) / indexSize : 0;
++ else if (mesh->m_vertexBuffer.stride)
++ drawableCount = quint64(mesh->m_vertexBuffer.data.size()) / mesh->m_vertexBuffer.stride;
++
++ for (const Mesh::Subset &subset : std::as_const(mesh->m_subsets)) {
++ if (quint64(subset.offset) + subset.count > drawableCount) {
++ qWarning("Mesh subset draws %u from %u, but only %llu are available", subset.count,
++ subset.offset, qulonglong(drawableCount));
++ return 0;
++ }
++ for (const Mesh::Lod &lod : subset.lods) {
++ if (quint64(lod.offset) + lod.count > drawableCount) {
++ qWarning("Mesh level of detail draws %u from %u, but only %llu are available",
++ lod.count, lod.offset, qulonglong(drawableCount));
++ return 0;
++ }
++ }
++ }
++
++ return header->sizeInBytes;
++ }
++
++ void MeshInternal::writeMeshHeader(QIODevice *device, const MeshDataHeader &header)
++ {
++ QDataStream outputStream(device);
++ outputStream.setByteOrder(QDataStream::LittleEndian);
++ outputStream.setFloatingPointPrecision(QDataStream::SinglePrecision);
++
++ outputStream << header.fileId << header.fileVersion << header.flags << header.sizeInBytes;
++ }
++
++ // The legacy, now-removed, insane mesh code used to use a "serialization"
++ // strategy with dumping memory, yet combined with with an in-memory layout
++ // that is different from what's in the file. In version 4 we no longer write
++ // out valid offset values (see the // legacy offset comments), because the new
++ // loader does not need them, and calculating them is not sensible, especially
++ // due to the different layouts. We still do the alignment padding, even though
++ // that's also legacy nonsense, but having that allows the reader not have to
++ // branch based on the version.
++
++ quint64 MeshInternal::writeMeshData(QIODevice *device, const Mesh &mesh)
++ {
++ static const char alignPadding[4] = {};
++
++ QDataStream outputStream(device);
++ outputStream.setByteOrder(QDataStream::LittleEndian);
++ outputStream.setFloatingPointPrecision(QDataStream::SinglePrecision);
++
++ // The buffers bypass the stream, so one flag covers their writes and is
++ // tested at the end with the stream's own status.
++ bool writesOk = true;
++ auto writeBytes = [&](const char *data, qint64 size) {
++ if (size > 0 && device->write(data, size) != size)
++ writesOk = false;
++ };
++
++ const qint64 startPos = device->pos();
++ MeshInternal::MeshOffsetTracker offsetTracker(startPos);
++ Q_ASSERT(offsetTracker.offset() == device->pos());
++
++ const quint32 vertexBufferEntriesCount = mesh.m_vertexBuffer.entries.size();
++ const quint32 vertexBufferDataSize = mesh.m_vertexBuffer.data.size();
++ const quint32 vertexBufferStride = mesh.m_vertexBuffer.stride;
++ const quint32 targetBufferEntriesCount = mesh.m_targetBuffer.entries.count();
++ const quint32 targetBufferDataSize = mesh.m_targetBuffer.data.size();
++ outputStream << targetBufferEntriesCount
++ << vertexBufferEntriesCount
++ << vertexBufferStride;
++ outputStream << targetBufferDataSize
++ << vertexBufferDataSize;
++
++ const quint32 indexBufferDataSize = mesh.m_indexBuffer.data.size();
++ const quint32 indexComponentType = quint32(mesh.m_indexBuffer.componentType);
++ outputStream << indexComponentType;
++ outputStream << quint32(0) // legacy offset
++ << indexBufferDataSize;
++
++ const quint32 targetCount = mesh.m_targetBuffer.numTargets;
++ const quint32 subsetsCount = mesh.m_subsets.size();
++ outputStream << targetCount
++ << subsetsCount;
++
++ outputStream << quint32(0) // legacy offset
++ << quint32(0); // legacy jointsCount
++
++ const quint32 drawMode = quint32(mesh.m_drawMode);
++ const quint32 winding = quint32(mesh.m_winding);
++ outputStream << drawMode
++ << winding;
++
++ offsetTracker.advance(MESH_STRUCT_SIZE);
++
++ quint32 entriesByteSize = 0;
++ for (quint32 i = 0; i < vertexBufferEntriesCount; ++i) {
++ const Mesh::VertexBufferEntry &entry(mesh.m_vertexBuffer.entries[i]);
++ const quint32 componentType = quint32(entry.componentType);
++ const quint32 componentCount = entry.componentCount;
++ const quint32 offset = entry.offset;
++ outputStream << quint32(0) // legacy offset
++ << componentType
++ << componentCount
++ << offset;
++ entriesByteSize += VERTEX_BUFFER_ENTRY_STRUCT_SIZE;
++ }
++ quint32 alignAmount = offsetTracker.alignedAdvance(entriesByteSize);
++ if (alignAmount)
++ writeBytes(alignPadding, alignAmount);
++
++ for (quint32 i = 0; i < vertexBufferEntriesCount; ++i) {
++ const Mesh::VertexBufferEntry &entry(mesh.m_vertexBuffer.entries[i]);
++ const quint32 nameLength = entry.name.size() + 1;
++ outputStream << nameLength;
++ writeBytes(entry.name.constData(), nameLength); // with zero terminator included
++ alignAmount = offsetTracker.alignedAdvance(sizeof(quint32) + nameLength);
++ if (alignAmount)
++ writeBytes(alignPadding, alignAmount);
++ }
++
++ writeBytes(mesh.m_vertexBuffer.data.constData(), vertexBufferDataSize);
++ alignAmount = offsetTracker.alignedAdvance(vertexBufferDataSize);
++ if (alignAmount)
++ writeBytes(alignPadding, alignAmount);
++
++ writeBytes(mesh.m_indexBuffer.data.constData(), indexBufferDataSize);
++ alignAmount = offsetTracker.alignedAdvance(indexBufferDataSize);
++ if (alignAmount)
++ writeBytes(alignPadding, alignAmount);
++
++ quint32 subsetByteSize = 0;
++ for (quint32 i = 0; i < subsetsCount; ++i) {
++ const Mesh::Subset &subset(mesh.m_subsets[i]);
++ const quint32 subsetCount = subset.count;
++ const quint32 subsetOffset = subset.offset;
++ const float minX = subset.bounds.min.x();
++ const float minY = subset.bounds.min.y();
++ const float minZ = subset.bounds.min.z();
++ const float maxX = subset.bounds.max.x();
++ const float maxY = subset.bounds.max.y();
++ const float maxZ = subset.bounds.max.z();
++ const quint32 nameLength = subset.name.size() + 1;
++ const quint32 lightmapSizeHintWidth = qMax(0, subset.lightmapSizeHint.width());
++ const quint32 lightmapSizeHintHeight = qMax(0, subset.lightmapSizeHint.height());
++ const quint32 lodCount = subset.lods.size();
++ outputStream << subsetCount
++ << subsetOffset
++ << minX
++ << minY
++ << minZ
++ << maxX
++ << maxY
++ << maxZ;
++ outputStream << quint32(0) // legacy offset
++ << nameLength;
++ outputStream << lightmapSizeHintWidth
++ << lightmapSizeHintHeight;
++ outputStream << lodCount;
++ subsetByteSize += SUBSET_STRUCT_SIZE_V6;
++ }
++ alignAmount = offsetTracker.alignedAdvance(subsetByteSize);
++ if (alignAmount)
++ writeBytes(alignPadding, alignAmount);
++
++ for (quint32 i = 0; i < subsetsCount; ++i) {
++ const Mesh::Subset &subset(mesh.m_subsets[i]);
++ const char *utf16Name = reinterpret_cast<const char *>(subset.name.utf16());
++ const quint32 nameByteSize = (subset.name.size() + 1) * 2;
++ writeBytes(utf16Name, nameByteSize);
++ alignAmount = offsetTracker.alignedAdvance(nameByteSize);
++ if (alignAmount)
++ writeBytes(alignPadding, alignAmount);
++ }
++
++ // LOD data
++ quint32 lodDataByteSize = 0;
++ for (quint32 i = 0; i < subsetsCount; ++i) {
++ const Mesh::Subset &subset(mesh.m_subsets[i]);
++ for (auto lod : subset.lods) {
++ const quint32 count = lod.count;
++ const quint32 offset = lod.offset;
++ const float distance = lod.distance;
++ outputStream << count << offset << distance;
++ lodDataByteSize += LOD_STRUCT_SIZE;
++ }
++ }
++ alignAmount = offsetTracker.alignedAdvance(lodDataByteSize);
++ if (alignAmount)
++ writeBytes(alignPadding, alignAmount);
++
++ // Data for morphTargets
++ for (quint32 i = 0; i < targetBufferEntriesCount; ++i) {
++ const Mesh::VertexBufferEntry &entry(mesh.m_targetBuffer.entries[i]);
++ const quint32 componentType = quint32(entry.componentType);
++ const quint32 componentCount = entry.componentCount;
++ const quint32 offset = entry.offset;
++ outputStream << quint32(0) // legacy offset
++ << componentType
++ << componentCount
++ << offset;
++ entriesByteSize += VERTEX_BUFFER_ENTRY_STRUCT_SIZE;
++ }
++ alignAmount = offsetTracker.alignedAdvance(entriesByteSize);
++ if (alignAmount)
++ writeBytes(alignPadding, alignAmount);
++
++ for (quint32 i = 0; i < targetBufferEntriesCount; ++i) {
++ const Mesh::VertexBufferEntry &entry(mesh.m_targetBuffer.entries[i]);
++ const quint32 nameLength = entry.name.size() + 1;
++ outputStream << nameLength;
++ writeBytes(entry.name.constData(), nameLength); // with zero terminator included
++ alignAmount = offsetTracker.alignedAdvance(sizeof(quint32) + nameLength);
++ if (alignAmount)
++ writeBytes(alignPadding, alignAmount);
++ }
++
++ writeBytes(mesh.m_targetBuffer.data.constData(), targetBufferDataSize);
++
++ if (!writesOk || outputStream.status() != QDataStream::Ok) {
++ qWarning("Failed to write mesh data");
++ return 0;
++ }
++
++ const quint32 endPos = device->pos();
++ const quint32 sizeInBytes = endPos - startPos;
++ device->seek(endPos);
++ return sizeInBytes;
++ }
++
++ Mesh Mesh::loadMesh(QIODevice *device, quint32 id)
++ {
++ MeshInternal::MeshDataHeader header;
++ const MeshInternal::MultiMeshInfo meshFileInfo = MeshInternal::readFileHeader(device);
++ auto it = meshFileInfo.meshEntries.constFind(id);
++ if (it != meshFileInfo.meshEntries.constEnd()) {
++ Mesh mesh;
++ quint64 size = MeshInternal::readMeshData(device, *it, &mesh, &header);
++ if (size)
++ return mesh;
++ } else if (id == 0 && !meshFileInfo.meshEntries.isEmpty()) {
++ Mesh mesh;
++ quint64 size = MeshInternal::readMeshData(device, *meshFileInfo.meshEntries.cbegin(), &mesh, &header);
++ if (size)
++ return mesh;
++ }
++ return Mesh();
++ }
++
++ QMap<quint32, Mesh> Mesh::loadAll(QIODevice *device)
++ {
++ MeshInternal::MeshDataHeader header;
++ const MeshInternal::MultiMeshInfo meshFileInfo = MeshInternal::readFileHeader(device);
++ QMap<quint32, Mesh> meshes;
++ for (auto it = meshFileInfo.meshEntries.cbegin(), end = meshFileInfo.meshEntries.cend(); it != end; ++it) {
++ Mesh mesh;
++ quint64 size = MeshInternal::readMeshData(device, *it, &mesh, &header);
++ if (size)
++ meshes.insert(it.key(), mesh);
++ else
++ qWarning("Failed to find mesh #%u", it.key());
++ }
++ return meshes;
++ }
++
++ static inline quint32 getAlignedOffset(quint32 offset, quint32 align)
++ {
++ Q_ASSERT(align > 0);
++ const quint32 leftover = (align > 0) ? offset % align : 0;
++ if (leftover)
++ return offset + (align - leftover);
++ return offset;
++ }
++
++ Mesh Mesh::fromAssetData(const QVector<AssetVertexEntry> &vbufEntries,
++ const QByteArray &indexBufferData,
++ ComponentType indexComponentType,
++ const QVector<AssetMeshSubset> &subsets,
++ quint32 numTargets,
++ quint32 numTargetComps)
++ {
++ Mesh mesh;
++ quint32 currentOffset = 0;
++ quint32 bufferAlignment = 0;
++ quint32 numItems = 0;
++ bool ok = true;
++
++ mesh.m_targetBuffer.numTargets = numTargets;
++ quint32 targetCurrentComp = 0;
++ quint32 targetCompStride = 0;
++
++ QVector<AssetVertexEntry> vEntries;
++ for (const AssetVertexEntry &entry : vbufEntries) {
++ // Ignore entries with no data.
++ if (entry.data.isEmpty())
++ continue;
++
++ VertexBufferEntry meshEntry;
++ meshEntry.componentType = entry.componentType;
++ meshEntry.componentCount = entry.componentCount;
++ meshEntry.name = entry.name;
++
++ if (entry.morphTargetId < 0) {
++ const quint32 alignment = MeshInternal::byteSizeForComponentType(entry.componentType);
++ const quint32 byteSize = alignment * entry.componentCount;
++
++ if (entry.data.size() % alignment != 0) {
++ Q_ASSERT(false);
++ ok = false;
++ }
++
++ quint32 localNumItems = entry.data.size() / byteSize;
++ if (numItems == 0) {
++ numItems = localNumItems;
++ } else if (numItems != localNumItems) {
++ Q_ASSERT(false);
++ ok = false;
++ numItems = qMin(numItems, localNumItems);
++ }
++
++ currentOffset = getAlignedOffset(currentOffset, alignment);
++ meshEntry.offset = currentOffset;
++
++ mesh.m_vertexBuffer.entries.append(meshEntry);
++ currentOffset += byteSize;
++ bufferAlignment = qMax(bufferAlignment, alignment);
++ vEntries.append(entry);
++ } else {
++ if (!targetCompStride) {
++ const quint32 targetEntrySize = entry.data.size();
++ quint32 targetEntryTexWidth = qCeil(qSqrt(((targetEntrySize + 15) >> 4)));
++ targetCompStride = targetEntryTexWidth * targetEntryTexWidth * 4 * sizeof(float);
++ mesh.m_targetBuffer.data.resize(targetCompStride * numTargets * numTargetComps);
++ }
++
++ // At assets, these entries are appended sequentially from target 0 to target N - 1
++ // It is safe to calculate the offset by the data size
++ meshEntry.offset = (targetCurrentComp * numTargets + entry.morphTargetId)
++ * targetCompStride;
++ memcpy(mesh.m_targetBuffer.data.data() + meshEntry.offset,
++ entry.data.constData(), entry.data.size());
++
++ // Note: the targetBuffer will not be interleaved,
++ // data will be just appended in order and used for a texture array.
++ if (entry.morphTargetId == 0)
++ mesh.m_targetBuffer.entries.append(meshEntry);
++
++ targetCurrentComp = (targetCurrentComp + 1 < numTargetComps) ? targetCurrentComp + 1 : 0;
++ }
++ }
++
++ if (!ok)
++ return Mesh();
++
++ mesh.m_vertexBuffer.stride = getAlignedOffset(currentOffset, bufferAlignment);
++
++ // Packed interleave the data.
++ for (quint32 idx = 0; idx < numItems; ++idx) {
++ quint32 dataOffset = 0;
++ for (const AssetVertexEntry &entry : vEntries) {
++ if (entry.data.isEmpty())
++ continue;
++
++ const quint32 alignment = MeshInternal::byteSizeForComponentType(entry.componentType);
++ const quint32 byteSize = alignment * entry.componentCount;
++ const quint32 offset = byteSize * idx;
++ const quint32 newOffset = getAlignedOffset(dataOffset, alignment);
++ if (newOffset != dataOffset) {
++ QByteArray filler(newOffset - dataOffset, '\0');
++ mesh.m_vertexBuffer.data.append(filler);
++ }
++
++ mesh.m_vertexBuffer.data.append(entry.data.constData() + offset, byteSize);
++ dataOffset = newOffset + byteSize;
++ }
++ Q_ASSERT(dataOffset == mesh.m_vertexBuffer.stride);
++ }
++
++ mesh.m_indexBuffer.componentType = indexComponentType;
++ mesh.m_indexBuffer.data = indexBufferData;
++
++ for (const AssetMeshSubset &subset : subsets) {
++ Mesh::Subset meshSubset;
++ meshSubset.name = subset.name;
++ meshSubset.count = subset.count;
++ meshSubset.offset = subset.offset;
++
++ // TODO: QTBUG-102026
++ if (subset.boundsPositionEntryIndex != std::numeric_limits<quint32>::max()) {
++ const QSSGBounds3 bounds = MeshInternal::calculateSubsetBounds(
++ mesh.m_vertexBuffer.entries[subset.boundsPositionEntryIndex],
++ mesh.m_vertexBuffer.data,
++ mesh.m_vertexBuffer.stride,
++ mesh.m_indexBuffer.data,
++ mesh.m_indexBuffer.componentType,
++ subset.count,
++ subset.offset);
++ meshSubset.bounds.min = bounds.minimum;
++ meshSubset.bounds.max = bounds.maximum;
++ }
++
++ meshSubset.lightmapSizeHint = QSize(subset.lightmapWidth, subset.lightmapHeight);
++ meshSubset.lods = subset.lods;
++
++ mesh.m_subsets.append(meshSubset);
++ }
++
++ mesh.m_drawMode = DrawMode::Triangles;
++ mesh.m_winding = Winding::CounterClockwise;
++
++ return mesh;
++ }
++
++ Mesh Mesh::fromRuntimeData(const RuntimeMeshData &data, QString *error)
++ {
++ if (data.m_vertexBuffer.size() == 0) {
++ *error = QObject::tr("Vertex buffer empty");
++ return Mesh();
++ }
++ if (data.m_attributeCount == 0) {
++ *error = QObject::tr("No attributes defined");
++ return Mesh();
++ }
++
++ Mesh mesh;
++ mesh.m_drawMode = data.m_primitiveType;
++ mesh.m_winding = Winding::CounterClockwise;
++
++ for (int i = 0; i < data.m_attributeCount; ++i) {
++ const RuntimeMeshData::Attribute &att = data.m_attributes[i];
++ if (att.semantic == RuntimeMeshData::Attribute::IndexSemantic) {
++ mesh.m_indexBuffer.componentType = att.componentType;
++ } else {
++ const char *name = nullptr;
++ switch (att.semantic) {
++ case RuntimeMeshData::Attribute::PositionSemantic:
++ name = MeshInternal::getPositionAttrName();
++ break;
++ case RuntimeMeshData::Attribute::NormalSemantic:
++ name = MeshInternal::getNormalAttrName();
++ break;
++ case RuntimeMeshData::Attribute::TexCoord0Semantic:
++ name = MeshInternal::getUV0AttrName();
++ break;
++ case RuntimeMeshData::Attribute::TexCoord1Semantic:
++ name = MeshInternal::getUV1AttrName();
++ break;
++ case RuntimeMeshData::Attribute::TangentSemantic:
++ name = MeshInternal::getTexTanAttrName();
++ break;
++ case RuntimeMeshData::Attribute::BinormalSemantic:
++ name = MeshInternal::getTexBinormalAttrName();
++ break;
++ case RuntimeMeshData::Attribute::JointSemantic:
++ name = MeshInternal::getJointAttrName();
++ break;
++ case RuntimeMeshData::Attribute::WeightSemantic:
++ name = MeshInternal::getWeightAttrName();
++ break;
++ case RuntimeMeshData::Attribute::ColorSemantic:
++ name = MeshInternal::getColorAttrName();
++ break;
++ default:
++ *error = QObject::tr("Warning: Invalid attribute semantic: %1")
++ .arg(att.semantic);
++ return Mesh();
++ }
++
++ VertexBufferEntry entry;
++ entry.componentType = att.componentType;
++ entry.componentCount = att.componentCount();
++ entry.offset = att.offset;
++ entry.name = name;
++ mesh.m_vertexBuffer.entries.append(entry);
++ }
++ }
++
++ mesh.m_vertexBuffer.data = data.m_vertexBuffer;
++ // Only interleaved vertex attribute packing is supported, both internally
++ // and in the QQuick3DGeometry API, hence the per-vertex buffer stride.
++ mesh.m_vertexBuffer.stride = data.m_stride;
++ mesh.m_subsets = data.m_subsets;
++ mesh.m_indexBuffer.data = data.m_indexBuffer;
++
++ if (!data.m_targetBuffer.isEmpty()) {
++ const quint32 vertexCount = data.m_vertexBuffer.size() / data.m_stride;
++ const quint32 targetEntryTexWidth = qCeil(qSqrt(vertexCount));
++ const quint32 targetCompStride = targetEntryTexWidth * targetEntryTexWidth * 4 * sizeof(float);
++ mesh.m_targetBuffer.data.resize(targetCompStride * data.m_targetAttributeCount);
++
++ QVarLengthArray<RuntimeMeshData::TargetAttribute> sortedAttribs(
++ data.m_targetAttributes,
++ data.m_targetAttributes + data.m_targetAttributeCount);
++ std::sort(sortedAttribs.begin(), sortedAttribs.end(),
++ [] (RuntimeMeshData::TargetAttribute a, RuntimeMeshData::TargetAttribute b) {
++ return (a.targetId == b.targetId) ? a.attr.semantic < b.attr.semantic :
++ a.targetId < b.targetId; });
++ for (int i = 0; i < data.m_targetAttributeCount; ++i) {
++ const RuntimeMeshData::Attribute &att = sortedAttribs[i].attr;
++ const int stride = (sortedAttribs[i].stride < 1) ? att.componentCount() * sizeof(float)
++ : sortedAttribs[i].stride;
++ const char *name = nullptr;
++ switch (att.semantic) {
++ case RuntimeMeshData::Attribute::PositionSemantic:
++ name = MeshInternal::getPositionAttrName();
++ break;
++ case RuntimeMeshData::Attribute::NormalSemantic:
++ name = MeshInternal::getNormalAttrName();
++ break;
++ case RuntimeMeshData::Attribute::TexCoord0Semantic:
++ name = MeshInternal::getUV0AttrName();
++ break;
++ case RuntimeMeshData::Attribute::TexCoord1Semantic:
++ name = MeshInternal::getUV1AttrName();
++ break;
++ case RuntimeMeshData::Attribute::TangentSemantic:
++ name = MeshInternal::getTexTanAttrName();
++ break;
++ case RuntimeMeshData::Attribute::BinormalSemantic:
++ name = MeshInternal::getTexBinormalAttrName();
++ break;
++ case RuntimeMeshData::Attribute::IndexSemantic:
++ case RuntimeMeshData::Attribute::JointSemantic:
++ case RuntimeMeshData::Attribute::WeightSemantic:
++ *error = QObject::tr("Warning: Invalid target attribute semantic: %1")
++ .arg(att.semantic);
++ continue;
++ case RuntimeMeshData::Attribute::ColorSemantic:
++ name = MeshInternal::getColorAttrName();
++ break;
++ default:
++ *error = QObject::tr("Warning: Invalid target attribute semantic: %1")
++ .arg(att.semantic);
++ return Mesh();
++ }
++ char *dstBuf = mesh.m_targetBuffer.data.data() + i * targetCompStride;
++ const char *srcBuf = data.m_targetBuffer.constData() + att.offset;
++ Q_ASSERT(att.componentType == Mesh::ComponentType::Float32);
++ if (stride == 4 * sizeof(float)) {
++ memcpy(dstBuf, srcBuf, vertexCount * stride);
++ } else {
++ for (quint32 j = 0; j < vertexCount; ++j) {
++ memcpy(dstBuf + j * 4 * sizeof(float),
++ srcBuf + j * stride,
++ att.componentCount() * sizeof(float));
++ }
++ }
++
++ if (sortedAttribs[i].targetId == 0) {
++ VertexBufferEntry entry;
++ entry.componentType = att.componentType;
++ entry.componentCount = att.componentCount();
++ entry.offset = i * targetCompStride;
++ entry.name = name;
++ mesh.m_targetBuffer.entries.append(entry);
++ }
++ }
++ mesh.m_targetBuffer.numTargets = data.m_targetAttributeCount / mesh.m_targetBuffer.entries.size();
++ }
++ return mesh;
++ }
++
++ quint32 Mesh::save(QIODevice *device, quint32 id) const
++ {
++ qint64 newMeshStartPosFromEnd = 0;
++ quint32 newId = 1;
++ MeshInternal::MultiMeshInfo header;
++
++ if (device->size() > 0) {
++ header = MeshInternal::readFileHeader(device);
++ if (!header.isValid()) {
++ qWarning("There is existing data, but mesh file header is invalid; cannot save");
++ return 0;
++ }
++ for (auto it = header.meshEntries.cbegin(), end = header.meshEntries.cend(); it != end; ++it) {
++ if (id) {
++ Q_ASSERT(id != it.key());
++ newId = id;
++ } else {
++ newId = qMax(newId, it.key() + 1);
++ }
++ }
++ newMeshStartPosFromEnd = MULTI_HEADER_STRUCT_SIZE + header.meshEntries.size() * MULTI_ENTRY_STRUCT_SIZE;
++ } else {
++ header = MeshInternal::MultiMeshInfo::withDefaults();
++ }
++
++ // the new mesh data overwrites the entry list and file header
++ device->seek(device->size() - newMeshStartPosFromEnd);
++ const qint64 meshOffset = device->pos();
++ header.meshEntries.insert(newId, meshOffset);
++
++ MeshInternal::MeshDataHeader meshHeader = MeshInternal::MeshDataHeader::withDefaults();
++ // skip the space for the mesh header for now
++ device->seek(device->pos() + MESH_HEADER_STRUCT_SIZE);
++ meshHeader.sizeInBytes = MeshInternal::writeMeshData(device, *this);
++ if (meshHeader.sizeInBytes == 0)
++ return 0;
++ // now the mesh header is ready to be written out
++ device->seek(meshOffset);
++ MeshInternal::writeMeshHeader(device, meshHeader);
++ device->seek(meshOffset + MESH_HEADER_STRUCT_SIZE + meshHeader.sizeInBytes);
++ // write out new entry list and file header
++ MeshInternal::writeFileHeader(device, header);
++
++ return newId;
++ }
++
++ QSSGBounds3 MeshInternal::calculateSubsetBounds(const Mesh::VertexBufferEntry &entry,
++ const QByteArray &vertexBufferData,
++ quint32 vertexBufferStride,
++ const QByteArray &indexBufferData,
++ Mesh::ComponentType indexComponentType,
++ quint32 subsetCount,
++ quint32 subsetOffset)
++ {
++ QSSGBounds3 result;
++ if (entry.componentType != Mesh::ComponentType::Float32 || entry.componentCount != 3) {
++ Q_ASSERT(false);
++ return result;
++ }
++
++ const int indexComponentByteSize = byteSizeForComponentType(indexComponentType);
++ if (indexComponentByteSize != 2 && indexComponentByteSize != 4) {
++ Q_ASSERT(false);
++ return result;
++ }
++
++ const quint32 indexBufferCount = indexBufferData.size() / indexComponentByteSize;
++ const quint32 vertexBufferByteSize = vertexBufferData.size();
++ const char *vertexSrcPtr = vertexBufferData.constData();
++ const char *indexSrcPtr = indexBufferData.constData();
++
++ for (quint32 idx = 0, numItems = subsetCount; idx < numItems; ++idx) {
++ if (idx + subsetOffset >= indexBufferCount)
++ continue;
++
++ quint32 vertexIdx = 0;
++ switch (indexComponentByteSize) {
++ case 2:
++ vertexIdx = reinterpret_cast<const quint16 *>(indexSrcPtr)[idx + subsetOffset];
++ break;
++ case 4:
++ vertexIdx = reinterpret_cast<const quint32 *>(indexSrcPtr)[idx + subsetOffset];
++ break;
++ default:
++ Q_UNREACHABLE();
++ break;
++ }
++
++ const quint32 finalOffset = entry.offset + (vertexIdx * vertexBufferStride);
++ float v[3];
++ if (finalOffset + sizeof(v) <= vertexBufferByteSize) {
++ memcpy(v, vertexSrcPtr + finalOffset, sizeof(v));
++ result.include(QVector3D(v[0], v[1], v[2]));
++ } else {
++ Q_ASSERT(false);
++ }
++ }
++
++ return result;
++ }
++
++ bool Mesh::hasLightmapUVChannel() const
++ {
++ const char *lightmapAttrName = MeshInternal::getLightmapUVAttrName();
++ for (const VertexBufferEntry &vbe : std::as_const(m_vertexBuffer.entries)) {
++ if (vbe.name == lightmapAttrName)
++ return true;
++ }
++ return false;
++ }
++
++ bool Mesh::createLightmapUVChannel(float texelsPerUnit, const QMatrix4x4 &scale)
++ {
++ const char *posAttrName = MeshInternal::getPositionAttrName();
++ const char *normalAttrName = MeshInternal::getNormalAttrName();
++ const char *uvAttrName = MeshInternal::getUV0AttrName();
++ const char *lightmapAttrName = MeshInternal::getLightmapUVAttrName();
++
++ // this function should do nothing if there is already an attr_lightmapuv
++ if (hasLightmapUVChannel())
++ return true;
++
++ const char *srcVertexData = m_vertexBuffer.data.constData();
++ const quint32 srcVertexStride = m_vertexBuffer.stride;
++ if (!srcVertexStride) {
++ qWarning("Lightmap UV unwrapping encountered a Mesh with 0 vertex stride, this cannot happen");
++ return false;
++ }
++ if (m_indexBuffer.data.isEmpty()) {
++ qWarning("Lightmap UV unwrapping encountered a Mesh without index data, this cannot happen");
++ return false;
++ }
++
++ quint32 positionOffset = UINT32_MAX;
++ quint32 normalOffset = UINT32_MAX;
++ quint32 uvOffset = UINT32_MAX;
++
++ for (const VertexBufferEntry &vbe : std::as_const(m_vertexBuffer.entries)) {
++ if (vbe.name == posAttrName) {
++ if (vbe.componentCount != 3) {
++ qWarning("Lightmap UV unwrapping encountered a Mesh non-float3 position data, this cannot happen");
++ return false;
++ }
++ positionOffset = vbe.offset;
++ } else if (vbe.name == normalAttrName) {
++ if (vbe.componentCount != 3) {
++ qWarning("Lightmap UV unwrapping encountered a Mesh non-float3 normal data, this cannot happen");
++ return false;
++ }
++ normalOffset = vbe.offset;
++ } else if (vbe.name == uvAttrName) {
++ if (vbe.componentCount != 2) {
++ qWarning("Lightmap UV unwrapping encountered a Mesh non-float2 UV0 data, this cannot happen");
++ return false;
++ }
++ uvOffset = vbe.offset;
++ }
++ }
++
++ if (positionOffset == UINT32_MAX) {
++ qWarning("Lightmap UV unwrapping encountered a Mesh without vertex positions, this cannot happen");
++ return false;
++ }
++ // normal and uv0 are optional
++
++ const qsizetype vertexCount = m_vertexBuffer.data.size() / srcVertexStride;
++ QByteArray positionData(vertexCount * 3 * sizeof(float), Qt::Uninitialized);
++ float *posPtr = reinterpret_cast<float *>(positionData.data());
++ for (qsizetype i = 0; i < vertexCount; ++i) {
++ const char *vertexBasePtr = srcVertexData + i * srcVertexStride;
++ const float *srcPos = reinterpret_cast<const float *>(vertexBasePtr + positionOffset);
++ QVector3D srcV;
++ srcV.setX(*srcPos++);
++ srcV.setY(*srcPos++);
++ srcV.setZ(*srcPos++);
++ // We scale the positions here, but not on the source mesh, so that the uv unwrapper works on
++ // the positions that the model will have in the scene after its scaling has been applied. This
++ // way the texels-per-unit will be correct.
++ srcV = scale.map(srcV);
++ *posPtr++ = srcV.x();
++ *posPtr++ = srcV.y();
++ *posPtr++ = srcV.z();
++ }
++
++ QByteArray normalData;
++ if (normalOffset != UINT32_MAX) {
++ normalData.resize(vertexCount * 3 * sizeof(float));
++ float *normPtr = reinterpret_cast<float *>(normalData.data());
++ for (qsizetype i = 0; i < vertexCount; ++i) {
++ const char *vertexBasePtr = srcVertexData + i * srcVertexStride;
++ const float *srcNormal = reinterpret_cast<const float *>(vertexBasePtr + normalOffset);
++ *normPtr++ = *srcNormal++;
++ *normPtr++ = *srcNormal++;
++ *normPtr++ = *srcNormal++;
++ }
++ }
++
++ QByteArray uvData;
++ if (uvOffset != UINT32_MAX) {
++ uvData.resize(vertexCount * 2 * sizeof(float));
++ float *uvPtr = reinterpret_cast<float *>(uvData.data());
++ for (qsizetype i = 0; i < vertexCount; ++i) {
++ const char *vertexBasePtr = srcVertexData + i * srcVertexStride;
++ const float *srcUv = reinterpret_cast<const float *>(vertexBasePtr + uvOffset);
++ *uvPtr++ = *srcUv++;
++ *uvPtr++ = *srcUv++;
++ }
++ }
++
++ QSSGLightmapUVGenerator uvGen;
++ QSSGLightmapUVGeneratorResult r = uvGen.run(positionData, normalData, uvData,
++ m_indexBuffer.data, m_indexBuffer.componentType,
++ texelsPerUnit);
++ if (!r.isValid())
++ return false;
++
++ // the result can have more (but never less) vertices than the input
++ const int newVertexCount = r.vertexMap.size();
++
++ // r.indexData contains the new index data that has the same number of elements as before
++ const quint32 *newIndex = reinterpret_cast<const quint32 *>(r.indexData.constData());
++ if (m_indexBuffer.componentType == QSSGMesh::Mesh::ComponentType::UnsignedInt32) {
++ if (r.indexData.size() != m_indexBuffer.data.size()) {
++ qWarning("Index buffer size mismatch after lightmap UV unwrapping");
++ return false;
++ }
++ quint32 *indexDst = reinterpret_cast<quint32 *>(m_indexBuffer.data.data());
++ memcpy(indexDst, newIndex, m_indexBuffer.data.size());
++ } else {
++ if (r.indexData.size() != m_indexBuffer.data.size() * 2) {
++ qWarning("Index buffer size mismatch after lightmap UV unwrapping");
++ return false;
++ }
++ quint16 *indexDst = reinterpret_cast<quint16 *>(m_indexBuffer.data.data());
++ for (size_t i = 0, count = m_indexBuffer.data.size() / sizeof(quint16); i != count; ++i)
++ *indexDst++ = *newIndex++;
++ }
++
++ QVarLengthArray<QByteArray, 8> newData;
++ newData.reserve(m_vertexBuffer.entries.size());
++
++ for (const VertexBufferEntry &vbe : std::as_const(m_vertexBuffer.entries)) {
++ const qsizetype byteSize = vbe.componentCount * MeshInternal::byteSizeForComponentType(vbe.componentType);
++ QByteArray data(byteSize * vertexCount, Qt::Uninitialized);
++ char *dst = data.data();
++ for (qsizetype i = 0; i < vertexCount; ++i) {
++ memcpy(dst, srcVertexData + i * srcVertexStride + vbe.offset, byteSize);
++ dst += byteSize;
++ }
++ switch (vbe.componentType) {
++ case ComponentType::UnsignedInt8:
++ newData.append(QSSGLightmapUVGenerator::remap<quint8>(data, r.vertexMap, vbe.componentCount));
++ break;
++ case ComponentType::Int8:
++ newData.append(QSSGLightmapUVGenerator::remap<qint8>(data, r.vertexMap, vbe.componentCount));
++ break;
++ case ComponentType::UnsignedInt16:
++ newData.append(QSSGLightmapUVGenerator::remap<quint16>(data, r.vertexMap, vbe.componentCount));
++ break;
++ case ComponentType::Int16:
++ newData.append(QSSGLightmapUVGenerator::remap<qint16>(data, r.vertexMap, vbe.componentCount));
++ break;
++ case ComponentType::UnsignedInt32:
++ newData.append(QSSGLightmapUVGenerator::remap<quint32>(data, r.vertexMap, vbe.componentCount));
++ break;
++ case ComponentType::Int32:
++ newData.append(QSSGLightmapUVGenerator::remap<qint32>(data, r.vertexMap, vbe.componentCount));
++ break;
++ case ComponentType::UnsignedInt64:
++ newData.append(QSSGLightmapUVGenerator::remap<quint64>(data, r.vertexMap, vbe.componentCount));
++ break;
++ case ComponentType::Int64:
++ newData.append(QSSGLightmapUVGenerator::remap<qint64>(data, r.vertexMap, vbe.componentCount));
++ break;
++ case ComponentType::Float16:
++ newData.append(QSSGLightmapUVGenerator::remap<qfloat16>(data, r.vertexMap, vbe.componentCount));
++ break;
++ case ComponentType::Float32:
++ newData.append(QSSGLightmapUVGenerator::remap<float>(data, r.vertexMap, vbe.componentCount));
++ break;
++ case ComponentType::Float64:
++ newData.append(QSSGLightmapUVGenerator::remap<double>(data, r.vertexMap, vbe.componentCount));
++ break;
++ }
++ }
++
++ VertexBufferEntry lightmapUVEntry;
++ lightmapUVEntry.componentType = ComponentType::Float32;
++ lightmapUVEntry.componentCount = 2;
++ lightmapUVEntry.offset = 0;
++ lightmapUVEntry.name = lightmapAttrName;
++
++ QByteArray newVertexBuffer;
++ newVertexBuffer.reserve(newVertexCount * (srcVertexStride + 8));
++
++ quint32 bufferAlignment = 0;
++ for (int vertexIdx = 0; vertexIdx < newVertexCount; ++vertexIdx) {
++ quint32 dataOffset = 0;
++ for (int vbIdx = 0, end = m_vertexBuffer.entries.size(); vbIdx != end; ++vbIdx) {
++ VertexBufferEntry &vbe(m_vertexBuffer.entries[vbIdx]);
++
++ const quint32 alignment = MeshInternal::byteSizeForComponentType(vbe.componentType);
++ bufferAlignment = qMax(bufferAlignment, alignment);
++ const quint32 byteSize = alignment * vbe.componentCount;
++ const quint32 newOffset = getAlignedOffset(dataOffset, alignment);
++
++ if (newOffset != dataOffset) {
++ QByteArray filler(newOffset - dataOffset, '\0');
++ newVertexBuffer.append(filler);
++ }
++
++ if (vertexIdx == 0)
++ vbe.offset = newVertexBuffer.size();
++
++ newVertexBuffer.append(newData[vbIdx].constData() + byteSize * vertexIdx, byteSize);
++ dataOffset = newOffset + byteSize;
++ }
++
++ const quint32 byteSize = 2 * sizeof(float);
++ const quint32 newOffset = getAlignedOffset(dataOffset, byteSize);
++ if (newOffset != dataOffset) {
++ QByteArray filler(newOffset - dataOffset, '\0');
++ newVertexBuffer.append(filler);
++ }
++
++ if (vertexIdx == 0)
++ lightmapUVEntry.offset = newVertexBuffer.size();
++
++ newVertexBuffer.append(r.lightmapUVChannel.constData() + byteSize * vertexIdx, byteSize);
++ dataOffset = newOffset + byteSize;
++
++ if (vertexIdx == 0)
++ m_vertexBuffer.stride = getAlignedOffset(dataOffset, bufferAlignment);
++ }
++
++ m_vertexBuffer.entries.append(lightmapUVEntry);
++
++ m_vertexBuffer.data = newVertexBuffer;
++
++ const QSize lightmapSizeHint(r.lightmapWidth, r.lightmapHeight);
++ for (Subset &subset : m_subsets)
++ subset.lightmapSizeHint = lightmapSizeHint;
++
++ return true;
++ }
++
++ size_t simplifyMesh(unsigned int *destination, const unsigned int *indices, size_t indexCount, const float *vertexPositions, size_t vertexCount, size_t vertexPositionsStride, size_t targetIndexCount, float targetError, unsigned int options, float *resultError)
++ {
++ return meshopt_simplify(destination, indices, indexCount, vertexPositions, vertexCount, vertexPositionsStride, targetIndexCount, targetError, options, resultError);
++ }
++
++ float simplifyScale(const float *vertexPositions, size_t vertexCount, size_t vertexPositionsStride)
++ {
++ return meshopt_simplifyScale(vertexPositions, vertexCount, vertexPositionsStride);
++ }
++
++ void optimizeVertexCache(unsigned int *destination, const unsigned int *indices, size_t indexCount, size_t vertexCount)
++ {
++ meshopt_optimizeVertexCache(destination, indices, indexCount, vertexCount);
++ }
++
++ size_t generateVertexRemap(unsigned int *destination,
++ const unsigned int *indices,
++ size_t indexCount,
++ size_t vertexCount,
++ const MeshVertexStream *streams,
++ size_t streamCount)
++ {
++ Q_ASSERT(streamCount > 0 && streamCount <= maxVertexStreams);
++ QVarLengthArray<meshopt_Stream, 16> meshoptStreams;
++ for (size_t i = 0; i < streamCount; ++i)
++ meshoptStreams.append({ streams[i].data, streams[i].elementSize, streams[i].stride });
++ return meshopt_generateVertexRemapMulti(destination, indices, indexCount, vertexCount,
++ meshoptStreams.constData(), streamCount);
++ }
++
++ void generateTangents(float *destination,
++ const unsigned int *indices,
++ size_t indexCount,
++ const float *vertexPositions,
++ size_t vertexCount,
++ size_t vertexPositionsStride,
++ const float *vertexNormals,
++ size_t vertexNormalsStride,
++ const float *vertexUvs,
++ size_t vertexUvsStride)
++ {
++ meshopt_generateTangents(destination, indices, indexCount,
++ vertexPositions, vertexCount, vertexPositionsStride,
++ vertexNormals, vertexNormalsStride,
++ vertexUvs, vertexUvsStride, 0);
++ }
++
++ void remapVertexBuffer(void *destination,
++ const void *vertices,
++ size_t vertexCount,
++ size_t vertexSize,
++ const unsigned int *remap)
++ {
++ meshopt_remapVertexBuffer(destination, vertices, vertexCount, vertexSize, remap);
++ }
++
++ void remapIndexBuffer(unsigned int *destination,
++ const unsigned int *indices,
++ size_t indexCount,
++ const unsigned int *remap)
++ {
++ meshopt_remapIndexBuffer(destination, indices, indexCount, remap);
++ }
++
++ QVector<MeshLevelOfDetail> generateMeshLevelsOfDetail(const QVector<QVector3D> &positions,
++ const QVector<QVector3D> &normals,
++ const QVector<quint32> &indexes,
++ QVector<MeshVertexSplit> &splitVertices,
++ float normalMergeAngle,
++ float normalSplitAngle)
++ {
++ // The split vertices are reported positionally, starting at
++ // positions.size(), so stale entries would make the caller's append loop
++ // line up with the wrong vertices
++ splitVertices.clear();
++
++ if (positions.isEmpty() || indexes.isEmpty())
++ return {};
++
++ // Correcting normals needs one per position. Both angles being 0.0 also
++ // means the caller does not want them recalculated at all.
++ const bool recalculateNormals = normals.size() == positions.size()
++ && !(qFuzzyIsNull(normalMergeAngle) && qFuzzyIsNull(normalSplitAngle));
++ const float normalMergeThreshold = qCos(qDegreesToRadians(normalMergeAngle));
++ const float normalSplitThreshold = qCos(qDegreesToRadians(normalSplitAngle));
++
++ quint32 splitVertexCount = positions.size();
++
++ // An edge can only be collapsed when its vertices are shared by the faces
++ // around it, so a vertex that appears more than once in the vertex buffer
++ // pins every edge that touches it. Assets are very commonly authored with
++ // one vertex per triangle corner - faceted normals and UV chart borders
++ // both force a split, and some exporters simply never weld - and such a
++ // mesh has no collapsible edge anywhere, so simplification returns the
++ // input unchanged and not a single level is produced. Weld by position to
++ // recover the real topology, simplify that, and translate the result back
++ // to the caller's vertex numbering afterwards, so the vertex buffer they
++ // hold is still the one the returned indexes refer to.
++ //
++ // Only the normals are rewritten below, so a position split to carry
++ // different normals can be welded, but one split to carry a different
++ // normal that has to be *kept* cannot: every face around it would be left
++ // reading whichever of them the weld happened to pick. So when the caller
++ // asked for the stored normals to be preserved, they join the weld key and
++ // a hard edge stays pinned, at the cost of fewer levels for such a mesh.
++ QVector<quint32> weldRemap(positions.size());
++ QVarLengthArray<MeshVertexStream, 2> streams;
++ streams.append({ positions.constData(), sizeof(QVector3D), sizeof(QVector3D) });
++ if (!recalculateNormals && normals.size() == positions.size())
++ streams.append({ normals.constData(), sizeof(QVector3D), sizeof(QVector3D) });
++ const quint32 weldedVertexCount = quint32(generateVertexRemap(weldRemap.data(), indexes.constData(),
++ indexes.size(),
++ positions.size(), streams.constData(),
++ size_t(streams.size())));
++ QVector<quint32> weldedIndexes(indexes.size());
++ remapIndexBuffer(weldedIndexes.data(), indexes.constData(), indexes.size(), weldRemap.constData());
++ QVector<QVector3D> weldedPositions(weldedVertexCount);
++ remapVertexBuffer(weldedPositions.data(), positions.constData(), positions.size(), sizeof(QVector3D),
++ weldRemap.constData());
++
++ // Pick one original vertex to stand for each welded one, so the simplified
++ // indexes can be mapped back. Every vertex welded together agrees on the
++ // attributes the weld key covers, so the choice only matters for the rest:
++ // where a position was split across a UV chart border the LOD has to settle
++ // on one of the charts either way.
++ constexpr quint32 unusedVertex = std::numeric_limits<quint32>::max();
++ QVector<quint32> weldedToOriginal(weldedVertexCount, unusedVertex);
++ for (quint32 i = 0, end = quint32(positions.size()); i < end; ++i) {
++ const quint32 welded = weldRemap.at(i);
++ // Vertices the index buffer never references are left unmapped
++ if (welded != unusedVertex && weldedToOriginal.at(welded) == unusedVertex)
++ weldedToOriginal[welded] = i;
++ }
++
++ const float targetError = std::numeric_limits<float>::max(); // error doesn't matter, index count is more important
++ const float *vertexData = reinterpret_cast<const float *>(weldedPositions.constData());
++ const float scaleFactor = simplifyScale(vertexData, weldedVertexCount, sizeof(QVector3D));
++ const quint32 indexCount = indexes.size();
++ // A level is kept when it is at least half again the previous one and still
++ // under three quarters of the mesh; simplifyMesh() rounds up to whole
++ // triangles, so a larger target only yields levels that get thrown away.
++ const quint32 maxLevelIndexes = indexCount - indexCount / 4 - 1;
++ const quint32 maxIndexTarget = maxLevelIndexes - maxLevelIndexes % 3;
++ quint32 indexTarget = 12;
++ quint32 lastIndexCount = 0;
++ QVector<MeshLevelOfDetail> lods;
++
++ while (indexTarget < indexCount) {
++ float error;
++ QVector<quint32> newIndexes;
++ newIndexes.resize(indexCount); // Must be the same size as the original indexes to pass to simplifyMesh
++ size_t newLength = simplifyMesh(newIndexes.data(), weldedIndexes.constData(), weldedIndexes.size(),
++ vertexData, weldedVertexCount, sizeof(QVector3D), indexTarget,
++ targetError, 0, &error);
++
++ // Not good enough, try again
++ if (newLength < lastIndexCount + (lastIndexCount + 1) / 2) {
++ const quint32 nextTarget = qMin(indexTarget + indexTarget / 2, maxIndexTarget);
++ if (nextTarget <= indexTarget)
++ break;
++ indexTarget = nextTarget;
++ continue;
++ }
++
++ // We are done
++ if (newLength == 0 || newLength > maxLevelIndexes)
++ break;
++
++ newIndexes.resize(newLength);
++
++ // Back to the caller's vertex numbering, which everything below - and
++ // the returned levels - is expressed in
++ for (quint32 &index : newIndexes)
++ index = weldedToOriginal.at(index);
++
++ // LOD Normal Correction
++ if (recalculateNormals) {
++ // Cull any new degenerate triangles and get the new face normals
++ QVector<QVector3D> faceNormals;
++ {
++ QVector<quint32> culledIndexes;
++ for (quint32 j = 0; j < quint32(newIndexes.size()); j += 3) {
++ const QVector3D &v0 = positions[newIndexes[j]];
++ const QVector3D &v1 = positions[newIndexes[j + 1]];
++ const QVector3D &v2 = positions[newIndexes[j + 2]];
++
++ QVector3D faceNormal = QVector3D::crossProduct(v1 - v0, v2 - v0);
++ // This normalizes the vector in place and returns the magnitude
++ const float faceArea = QSSGUtils::vec3::normalize(faceNormal);
++ // It is possible that the simplifyMesh process gave us a degenerate triangle
++ // (all three at the same point, or on the same line) or such a small triangle
++ // that a float value doesn't have enough resolution. In that case cull the
++ // "face" since it would not get rendered in a meaningful way anyway
++ if (faceArea != 0.0f) {
++ faceNormals.append(faceNormal);
++ faceNormals.append(faceNormal);
++ faceNormals.append(faceNormal);
++ culledIndexes.append({newIndexes[j], newIndexes[j + 1], newIndexes[j + 2]});
++ }
++ }
++
++ if (newIndexes.size() != culledIndexes.size())
++ newIndexes = culledIndexes;
++ }
++
++ // Group all shared vertices together by position. We need to know adjacent faces
++ // to do vertex normal remapping in the next step.
++ const quint32 newIndexCount = quint32(newIndexes.size());
++ QHash<QVector3D, QVector<quint32>> positionHash;
++ for (quint32 i = 0; i < newIndexCount; ++i) {
++ const quint32 index = newIndexes[i];
++ const QVector3D position = positions[index];
++ positionHash[position].append(i);
++ }
++
++ // Go through each vertex and calculate the normals by checking each
++ // adjacent face that share the same vertex position, and create a smoothed
++ // normal if the angle between thew face normals is less than the the
++ // normalMergeAngle passed to this function (>= since this is cos(radian(angle)) )
++ QVector<QPair<quint32, quint32>> remapIndexes;
++ for (quint32 positionIndex = 0; positionIndex < newIndexCount; ++positionIndex) {
++ const quint32 index = newIndexes[positionIndex];
++ const QVector3D &position = positions[index];
++ const QVector3D &faceNormal = faceNormals[positionIndex];
++ QVector3D newNormal;
++ // Find all vertices that share the same position
++ const auto &sharedPositions = positionHash.value(position);
++ for (const auto positionIndex2 : sharedPositions) {
++ if (positionIndex == positionIndex2) {
++ // Don't test against the current face under test
++ newNormal += faceNormal;
++ } else {
++ const QVector3D &faceNormal2 = faceNormals[positionIndex2];
++ if (QVector3D::dotProduct(faceNormal2, faceNormal) >= normalMergeThreshold)
++ newNormal += faceNormal2;
++ }
++ }
++
++ // By normalizing here we get an averaged value of all smoothed normals
++ QSSGUtils::vec3::normalize(newNormal);
++
++ // Now that we know what the smoothed normal would be, check how differnt
++ // that normal is from the normal that is already stored in the current
++ // index. If the angle delta is greater than normalSplitAngle then we need
++ // to create a new vertex entry (making a copy of the current one) and set
++ // the new normal value, and reassign the current index to point to that new
++ // vertex. Generally the LOD simplification process is such that the existing
++ // normal will already be ideal until we start getting to the very low lod levels
++ // which changes the topology in such a way that the original normal doesn't
++ // make sense anymore, thus the need to provide a more reasonable value.
++ const QVector3D &originalNormal = normals[index];
++ const float theta = QVector3D::dotProduct(originalNormal, newNormal);
++ if (theta < normalSplitThreshold) {
++ splitVertices.append({ index, newNormal.normalized() });
++ remapIndexes.append({positionIndex, splitVertexCount++});
++ }
++ }
++
++ // Do index remap now that all new normals have been calculated
++ for (const auto &pair : std::as_const(remapIndexes))
++ newIndexes[pair.first] = pair.second;
++ }
++
++ lods.append({error * scaleFactor, newIndexes});
++ lastIndexCount = quint32(newLength);
++
++ // No level can be both big enough and small enough any more
++ if (lastIndexCount + (lastIndexCount + 1) / 2 > maxLevelIndexes)
++ break;
++
++ const size_t doubled = qMax(newLength, size_t(indexTarget)) * 2; // may exceed quint32
++ const quint32 nextTarget = quint32(qMin<size_t>(doubled, maxIndexTarget));
++ if (nextTarget <= indexTarget)
++ break;
++ indexTarget = nextTarget;
++
++ if (error == 0.0f)
++ break;
++ }
++
++ return lods;
++ }
++
++ } // namespace QSSGMesh
++
++ QT_END_NAMESPACE