//// FBXSerializer.cpp// libraries/model-serializers/src//// Created by Andrzej Kapolka on 9/18/13.// Copyright 2013 High Fidelity, Inc.// Copyright 2022-2023 Overte e.V.//// Distributed under the Apache License, Version 2.0.// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html// #include "FBXSerializer.h" #include <QBuffer>#include <QRegularExpression> #include <glm/gtc/quaternion.hpp>#include <glm/gtx/quaternion.hpp>#include <glm/gtx/transform.hpp> #include <BlendshapeConstants.h> #include <hfm/ModelFormatLogging.h> // TOOL: Uncomment the following line to enable the filtering of all the unknown fields of a node so we can break point easily while loading a model with problems...//#define DEBUG_FBXSERIALIZER using namespace std; glm::vec3 parseVec3(const QString& string) { QStringList elements = string.split(','); if (elements.isEmpty()) { return glm::vec3(); } glm::vec3 value; for (int i = 0; i < 3; i++) { // duplicate last value if there aren't three elements value[i] = elements.at(min(i, elements.size() - 1)).trimmed().toFloat(); } return value;} enum RotationOrder { OrderXYZ = 0, OrderXZY, OrderYZX, OrderYXZ, OrderZXY, OrderZYX, OrderSphericXYZ}; bool haveReportedUnhandledRotationOrder = false; // Report error only once per FBX file. glm::vec3 convertRotationToXYZ(int rotationOrder, const glm::vec3& rotation) { // Convert rotation with given rotation order to have order XYZ. if (rotationOrder == OrderXYZ) { return rotation; } glm::quat xyzRotation; switch (rotationOrder) { case OrderXZY: xyzRotation = glm::quat(glm::radians(glm::vec3(0, rotation.y, 0))) * (glm::quat(glm::radians(glm::vec3(0, 0, rotation.z))) * glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0)))); break; case OrderYZX: xyzRotation = glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0))) * (glm::quat(glm::radians(glm::vec3(0, 0, rotation.z))) * glm::quat(glm::radians(glm::vec3(0, rotation.y, 0)))); break; case OrderYXZ: xyzRotation = glm::quat(glm::radians(glm::vec3(0, 0, rotation.z))) * (glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0))) * glm::quat(glm::radians(glm::vec3(0, rotation.y, 0)))); break; case OrderZXY: xyzRotation = glm::quat(glm::radians(glm::vec3(0, rotation.y, 0))) * (glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0))) * glm::quat(glm::radians(glm::vec3(0, 0, rotation.z)))); break; case OrderZYX: xyzRotation = glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0))) * (glm::quat(glm::radians(glm::vec3(0, rotation.y, 0))) * glm::quat(glm::radians(glm::vec3(0, 0, rotation.z)))); break; default: // FIXME: Handle OrderSphericXYZ. if (!haveReportedUnhandledRotationOrder) { qCDebug(modelformat) << "ERROR: Unhandled rotation order in FBX file:" << rotationOrder; haveReportedUnhandledRotationOrder = true; } return rotation; } return glm::degrees(safeEulerAngles(xyzRotation));} QString processID(const QString& id) { // Blender (at least) prepends a type to the ID, so strip it out return id.mid(id.lastIndexOf(':') + 1);} QString getModelName(const QVariantList& properties) { QString name; if (properties.size() == 3) { name = properties.at(1).toString(); name = processID(name.left(name.indexOf(QChar('\0')))); } else { name = processID(properties.at(0).toString()); } return name;} QString getMaterialName(const QVariantList& properties) { QString name; if (properties.size() == 1 || properties.at(1).toString().isEmpty()) { name = properties.at(0).toString(); name = processID(name.left(name.indexOf(QChar('\0')))); } else { name = processID(properties.at(1).toString()); } return name;} QString getID(const QVariantList& properties, int index = 0) { return processID(properties.at(index).toString());} class FBXModel {public: QString name; int parentIndex; glm::vec3 translation; glm::mat4 preTransform; glm::quat preRotation; glm::quat rotation; glm::quat postRotation; glm::mat4 postTransform; glm::vec3 rotationMin; // radians glm::vec3 rotationMax; // radians bool hasGeometricOffset; glm::vec3 geometricTranslation; glm::quat geometricRotation; glm::vec3 geometricScaling; bool isLimbNode; // is this FBXModel transform is a "LimbNode" i.e. a joint}; glm::mat4 getGlobalTransform(const QMultiMap<QString, QString>& _connectionParentMap, const QHash<QString, FBXModel>& fbxModels, QString nodeID, bool mixamoHack, const QString& url) { glm::mat4 globalTransform; QVector<QString> visitedNodes; // Used to prevent following a cycle while (!nodeID.isNull()) { visitedNodes.append(nodeID); // Append each node we visit const FBXModel& fbxModel = fbxModels.value(nodeID); globalTransform = glm::translate(fbxModel.translation) * fbxModel.preTransform * glm::mat4_cast(fbxModel.preRotation * fbxModel.rotation * fbxModel.postRotation) * fbxModel.postTransform * globalTransform; if (fbxModel.hasGeometricOffset) { glm::mat4 geometricOffset = createMatFromScaleQuatAndPos(fbxModel.geometricScaling, fbxModel.geometricRotation, fbxModel.geometricTranslation); globalTransform = globalTransform * geometricOffset; } if (mixamoHack) { // there's something weird about the models from Mixamo Fuse; they don't skin right with the full transform return globalTransform; } QList<QString> parentIDs = _connectionParentMap.values(nodeID); nodeID = QString(); foreach (const QString& parentID, parentIDs) { if (visitedNodes.contains(parentID)) { qCWarning(modelformat) << "Ignoring loop detected in FBX connection map for" << url; continue; } if (fbxModels.contains(parentID)) { nodeID = parentID; break; } } } return globalTransform;} class ExtractedBlendshape {public: QString id; HFMBlendshape blendshape;}; void printNode(const FBXNode& node, int indentLevel) { int indentLength = 2; hifi::ByteArray spaces(indentLevel * indentLength, ' '); QDebug nodeDebug = qDebug(modelformat); nodeDebug.nospace() << spaces.data() << node.name.data() << ": "; foreach (const QVariant& property, node.properties) { nodeDebug << property; } foreach (const FBXNode& child, node.children) { printNode(child, indentLevel + 1); }} class Cluster {public: QVector<int> indices; QVector<double> weights; glm::mat4 transformLink;}; void appendModelIDs(const QString& parentID, const QMultiMap<QString, QString>& connectionChildMap, QHash<QString, FBXModel>& fbxModels, QSet<QString>& remainingModels, QVector<QString>& modelIDs, bool isRootNode = false) { if (remainingModels.contains(parentID)) { modelIDs.append(parentID); remainingModels.remove(parentID); } int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; foreach (const QString& childID, connectionChildMap.values(parentID)) { if (remainingModels.contains(childID)) { FBXModel& fbxModel = fbxModels[childID]; if (fbxModel.parentIndex == -1) { fbxModel.parentIndex = parentIndex; appendModelIDs(childID, connectionChildMap, fbxModels, remainingModels, modelIDs); } } }} HFMBlendshape extractBlendshape(const FBXNode& object) { HFMBlendshape blendshape; foreach (const FBXNode& data, object.children) { if (data.name == "Indexes") { blendshape.indices = FBXSerializer::getIntVector(data); } else if (data.name == "Vertices") { blendshape.vertices = FBXSerializer::createVec3Vector(FBXSerializer::getDoubleVector(data)); } else if (data.name == "Normals") { blendshape.normals = FBXSerializer::createVec3Vector(FBXSerializer::getDoubleVector(data)); } } return blendshape;} QVector<int> getIndices(const QVector<QString> ids, QVector<QString> modelIDs) { QVector<int> indices; foreach (const QString& id, ids) { int index = modelIDs.indexOf(id); if (index != -1) { indices.append(index); } } return indices;} typedef QPair<int, float> WeightedIndex; void addBlendshapes(const ExtractedBlendshape& extracted, const QList<WeightedIndex>& indices, ExtractedMesh& extractedMesh) { foreach (const WeightedIndex& index, indices) { extractedMesh.mesh.blendshapes.resize(max(extractedMesh.mesh.blendshapes.size(), index.first + 1)); extractedMesh.blendshapeIndexMaps.resize(extractedMesh.mesh.blendshapes.size()); HFMBlendshape& blendshape = extractedMesh.mesh.blendshapes[index.first]; QHash<int, int>& blendshapeIndexMap = extractedMesh.blendshapeIndexMaps[index.first]; for (int i = 0; i < extracted.blendshape.indices.size(); i++) { int oldIndex = extracted.blendshape.indices.at(i); for (QMultiHash<int, int>::const_iterator it = extractedMesh.newIndices.constFind(oldIndex); it != extractedMesh.newIndices.constEnd() && it.key() == oldIndex; it++) { QHash<int, int>::iterator blendshapeIndex = blendshapeIndexMap.find(it.value()); if (blendshapeIndex == blendshapeIndexMap.end()) { blendshapeIndexMap.insert(it.value(), blendshape.indices.size()); blendshape.indices.append(it.value()); blendshape.vertices.append(extracted.blendshape.vertices.at(i) * index.second); blendshape.normals.append(extracted.blendshape.normals.at(i) * index.second); } else { blendshape.vertices[*blendshapeIndex] += extracted.blendshape.vertices.at(i) * index.second; blendshape.normals[*blendshapeIndex] += extracted.blendshape.normals.at(i) * index.second; } } } }} QString getTopModelID(const QMultiMap<QString, QString>& connectionParentMap, const QHash<QString, FBXModel>& fbxModels, const QString& modelID, const QString& url) { QString topID = modelID; QVector<QString> visitedNodes; // Used to prevent following a cycle forever { visitedNodes.append(topID); // Append each node we visit foreach (const QString& parentID, connectionParentMap.values(topID)) { if (visitedNodes.contains(parentID)) { qCWarning(modelformat) << "Ignoring loop detected in FBX connection map for" << url; continue; } if (fbxModels.contains(parentID)) { topID = parentID; goto outerContinue; } } return topID; outerContinue: ; }} QString getString(const QVariant& value) { // if it's a list, return the first entry QVariantList list = value.toList(); return list.isEmpty() ? value.toString() : list.at(0).toString();} class AnimationCurve {public: QVector<float> values;}; bool checkMaterialsHaveTextures(const QHash<QString, HFMMaterial>& materials, const QHash<QString, hifi::ByteArray>& textureFilenames, const QMultiMap<QString, QString>& _connectionChildMap) { foreach (const QString& materialID, materials.keys()) { foreach (const QString& childID, _connectionChildMap.values(materialID)) { if (textureFilenames.contains(childID)) { return true; } } } return false;} int matchTextureUVSetToAttributeChannel(const QString& texUVSetName, const QHash<QString, int>& texcoordChannels) { if (texUVSetName.isEmpty()) { return 0; } else { QHash<QString, int>::const_iterator tcUnit = texcoordChannels.find(texUVSetName); if (tcUnit != texcoordChannels.end()) { int channel = (*tcUnit); if (channel >= 2) { channel = 0; } return channel; } else { return 0; } }} HFMLight extractLight(const FBXNode& object) { HFMLight light; foreach (const FBXNode& subobject, object.children) { QString childname = QString(subobject.name); if (subobject.name == "Properties70") { foreach (const FBXNode& property, subobject.children) { int valIndex = 4; QString propName = QString(property.name); if (property.name == "P") { QString propname = property.properties.at(0).toString(); if (propname == "Intensity") { light.intensity = 0.01f * property.properties.at(valIndex).value<float>(); } else if (propname == "Color") { light.color = FBXSerializer::getVec3(property.properties, valIndex); } } } } else if ( subobject.name == "GeometryVersion" || subobject.name == "TypeFlags") { } }#if defined(DEBUG_FBXSERIALIZER) QString type = object.properties.at(0).toString(); type = object.properties.at(1).toString(); type = object.properties.at(2).toString(); foreach (const QVariant& prop, object.properties) { QString proptype = prop.typeName(); QString propval = prop.toString(); if (proptype == "Properties70") { } }#endif return light;} hifi::ByteArray fileOnUrl(const hifi::ByteArray& filepath, const QString& url) { // in order to match the behaviour when loading models from remote URLs // we assume that all external textures are right beside the loaded model // ignoring any relative paths or absolute paths inside of models return filepath.mid(filepath.lastIndexOf('/') + 1);} HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const QString& url) { const FBXNode& node = _rootNode; bool deduplicateIndices = mapping["deduplicateIndices"].toBool(); QMap<QString, ExtractedMesh> meshes; QHash<QString, QString> modelIDsToNames; QHash<QString, int> meshIDsToMeshIndices; QHash<QString, QString> ooChildToParent; QVector<ExtractedBlendshape> blendshapes; QHash<QString, FBXModel> fbxModels; QHash<QString, Cluster> clusters; QHash<QString, AnimationCurve> animationCurves; QHash<QString, QString> typeFlags; QHash<QString, QString> localRotations; QHash<QString, QString> localTranslations; QHash<QString, QString> xComponents; QHash<QString, QString> yComponents; QHash<QString, QString> zComponents; std::map<QString, HFMLight> lights; hifi::VariantMultiHash blendshapeMappings = mapping.value("bs").toHash(); QMultiHash<hifi::ByteArray, WeightedIndex> blendshapeIndices; for (int i = 0;; i++) { hifi::ByteArray blendshapeName = BLENDSHAPE_NAMES[i]; if (blendshapeName.isEmpty()) { break; } QList<QVariant> mappings = blendshapeMappings.values(blendshapeName); if (mappings.isEmpty()) { blendshapeIndices.insert(blendshapeName, WeightedIndex(i, 1.0f)); } else { foreach (const QVariant& mapping, mappings) { QVariantList blendshapeMapping = mapping.toList(); blendshapeIndices.insert(blendshapeMapping.at(0).toByteArray(), WeightedIndex(i, blendshapeMapping.at(1).toFloat())); } } } QMultiHash<QString, WeightedIndex> blendshapeChannelIndices;#if defined(DEBUG_FBXSERIALIZER) int unknown = 0;#endif HFMModel* hfmModelPtr = new HFMModel; HFMModel& hfmModel = *hfmModelPtr; hfmModel.originalURL = url; float unitScaleFactor = 1.0f; glm::quat upAxisZRotation; bool applyUpAxisZRotation = false; glm::vec3 ambientColor; QString hifiGlobalNodeID; unsigned int meshIndex = 0; haveReportedUnhandledRotationOrder = false; int fbxVersionNumber = -1; bool isBlenderVersionLower280 = false; foreach (const FBXNode& child, node.children) { if (child.name == "Creator") { // Match Blender version lower than 2.80 // Example version string from Blender 2.78: "Blender (stable FBX IO) - 2.78 (sub 0) - 3.7.7" // Example version string from Blender 2.92: "Blender (stable FBX IO) - 2.92.0 - 4.22.0" QRegularExpression blenderVersionRegex(".*Blender.*-\\s(\\d+)\\.(\\d+).*-.*"); QRegularExpressionMatch blenderVersionMatch = blenderVersionRegex.match(child.properties.at(0).toString()); if (blenderVersionMatch.hasMatch()) { int major = blenderVersionMatch.captured(1).toInt(); int minor = blenderVersionMatch.captured(2).toInt(); isBlenderVersionLower280 = major < 2 || (major == 2 && minor < 80); } } } foreach (const FBXNode& child, node.children) { if (child.name == "FBXHeaderExtension") { foreach (const FBXNode& object, child.children) { if (object.name == "SceneInfo") { foreach (const FBXNode& subobject, object.children) { if (subobject.name == "MetaData") { foreach (const FBXNode& subsubobject, subobject.children) { if (subsubobject.name == "Author") { hfmModel.author = subsubobject.properties.at(0).toString(); } } } else if (subobject.name == "Properties70") { foreach (const FBXNode& subsubobject, subobject.children) { static const QVariant APPLICATION_NAME = QVariant(hifi::ByteArray("Original|ApplicationName")); if (subsubobject.name == "P" && subsubobject.properties.size() >= 5 && subsubobject.properties.at(0) == APPLICATION_NAME) { hfmModel.applicationName = subsubobject.properties.at(4).toString(); } } } } } else if (object.name == "FBXVersion") { fbxVersionNumber = object.properties.at(0).toInt();Lines 1–500 of 1714. Load more
// // FBXSerializer.cpp // libraries/model-serializers/src // // Created by Andrzej Kapolka on 9/18/13. // Copyright 2013 High Fidelity, Inc. // Copyright 2022-2023 Overte e.V. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include "FBXSerializer.h" #include <QBuffer> #include <QRegularExpression> #include <glm/gtc/quaternion.hpp> #include <glm/gtx/quaternion.hpp> #include <glm/gtx/transform.hpp> #include <BlendshapeConstants.h> #include <hfm/ModelFormatLogging.h> // TOOL: Uncomment the following line to enable the filtering of all the unknown fields of a node so we can break point easily while loading a model with problems... //#define DEBUG_FBXSERIALIZER using namespace std; glm::vec3 parseVec3(const QString& string) { QStringList elements = string.split(','); if (elements.isEmpty()) { return glm::vec3(); } glm::vec3 value; for (int i = 0; i < 3; i++) { // duplicate last value if there aren't three elements value[i] = elements.at(min(i, elements.size() - 1)).trimmed().toFloat(); } return value; } enum RotationOrder { OrderXYZ = 0, OrderXZY, OrderYZX, OrderYXZ, OrderZXY, OrderZYX, OrderSphericXYZ }; bool haveReportedUnhandledRotationOrder = false; // Report error only once per FBX file. glm::vec3 convertRotationToXYZ(int rotationOrder, const glm::vec3& rotation) { // Convert rotation with given rotation order to have order XYZ. if (rotationOrder == OrderXYZ) { return rotation; } glm::quat xyzRotation; switch (rotationOrder) { case OrderXZY: xyzRotation = glm::quat(glm::radians(glm::vec3(0, rotation.y, 0))) * (glm::quat(glm::radians(glm::vec3(0, 0, rotation.z))) * glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0)))); break; case OrderYZX: xyzRotation = glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0))) * (glm::quat(glm::radians(glm::vec3(0, 0, rotation.z))) * glm::quat(glm::radians(glm::vec3(0, rotation.y, 0)))); break; case OrderYXZ: xyzRotation = glm::quat(glm::radians(glm::vec3(0, 0, rotation.z))) * (glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0))) * glm::quat(glm::radians(glm::vec3(0, rotation.y, 0)))); break; case OrderZXY: xyzRotation = glm::quat(glm::radians(glm::vec3(0, rotation.y, 0))) * (glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0))) * glm::quat(glm::radians(glm::vec3(0, 0, rotation.z)))); break; case OrderZYX: xyzRotation = glm::quat(glm::radians(glm::vec3(rotation.x, 0, 0))) * (glm::quat(glm::radians(glm::vec3(0, rotation.y, 0))) * glm::quat(glm::radians(glm::vec3(0, 0, rotation.z)))); break; default: // FIXME: Handle OrderSphericXYZ. if (!haveReportedUnhandledRotationOrder) { qCDebug(modelformat) << "ERROR: Unhandled rotation order in FBX file:" << rotationOrder; haveReportedUnhandledRotationOrder = true; } return rotation; } return glm::degrees(safeEulerAngles(xyzRotation)); } QString processID(const QString& id) { // Blender (at least) prepends a type to the ID, so strip it out return id.mid(id.lastIndexOf(':') + 1); } QString getModelName(const QVariantList& properties) { QString name; if (properties.size() == 3) { name = properties.at(1).toString(); name = processID(name.left(name.indexOf(QChar('\0')))); } else { name = processID(properties.at(0).toString()); } return name; } QString getMaterialName(const QVariantList& properties) { QString name; if (properties.size() == 1 || properties.at(1).toString().isEmpty()) { name = properties.at(0).toString(); name = processID(name.left(name.indexOf(QChar('\0')))); } else { name = processID(properties.at(1).toString()); } return name; } QString getID(const QVariantList& properties, int index = 0) { return processID(properties.at(index).toString()); } class FBXModel { public: QString name; int parentIndex; glm::vec3 translation; glm::mat4 preTransform; glm::quat preRotation; glm::quat rotation; glm::quat postRotation; glm::mat4 postTransform; glm::vec3 rotationMin; // radians glm::vec3 rotationMax; // radians bool hasGeometricOffset; glm::vec3 geometricTranslation; glm::quat geometricRotation; glm::vec3 geometricScaling; bool isLimbNode; // is this FBXModel transform is a "LimbNode" i.e. a joint }; glm::mat4 getGlobalTransform(const QMultiMap<QString, QString>& _connectionParentMap, const QHash<QString, FBXModel>& fbxModels, QString nodeID, bool mixamoHack, const QString& url) { glm::mat4 globalTransform; QVector<QString> visitedNodes; // Used to prevent following a cycle while (!nodeID.isNull()) { visitedNodes.append(nodeID); // Append each node we visit const FBXModel& fbxModel = fbxModels.value(nodeID); globalTransform = glm::translate(fbxModel.translation) * fbxModel.preTransform * glm::mat4_cast(fbxModel.preRotation * fbxModel.rotation * fbxModel.postRotation) * fbxModel.postTransform * globalTransform; if (fbxModel.hasGeometricOffset) { glm::mat4 geometricOffset = createMatFromScaleQuatAndPos(fbxModel.geometricScaling, fbxModel.geometricRotation, fbxModel.geometricTranslation); globalTransform = globalTransform * geometricOffset; } if (mixamoHack) { // there's something weird about the models from Mixamo Fuse; they don't skin right with the full transform return globalTransform; } QList<QString> parentIDs = _connectionParentMap.values(nodeID); nodeID = QString(); foreach (const QString& parentID, parentIDs) { if (visitedNodes.contains(parentID)) { qCWarning(modelformat) << "Ignoring loop detected in FBX connection map for" << url; continue; } if (fbxModels.contains(parentID)) { nodeID = parentID; break; } } } return globalTransform; } class ExtractedBlendshape { public: QString id; HFMBlendshape blendshape; }; void printNode(const FBXNode& node, int indentLevel) { int indentLength = 2; hifi::ByteArray spaces(indentLevel * indentLength, ' '); QDebug nodeDebug = qDebug(modelformat); nodeDebug.nospace() << spaces.data() << node.name.data() << ": "; foreach (const QVariant& property, node.properties) { nodeDebug << property; } foreach (const FBXNode& child, node.children) { printNode(child, indentLevel + 1); } } class Cluster { public: QVector<int> indices; QVector<double> weights; glm::mat4 transformLink; }; void appendModelIDs(const QString& parentID, const QMultiMap<QString, QString>& connectionChildMap, QHash<QString, FBXModel>& fbxModels, QSet<QString>& remainingModels, QVector<QString>& modelIDs, bool isRootNode = false) { if (remainingModels.contains(parentID)) { modelIDs.append(parentID); remainingModels.remove(parentID); } int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; foreach (const QString& childID, connectionChildMap.values(parentID)) { if (remainingModels.contains(childID)) { FBXModel& fbxModel = fbxModels[childID]; if (fbxModel.parentIndex == -1) { fbxModel.parentIndex = parentIndex; appendModelIDs(childID, connectionChildMap, fbxModels, remainingModels, modelIDs); } } } } HFMBlendshape extractBlendshape(const FBXNode& object) { HFMBlendshape blendshape; foreach (const FBXNode& data, object.children) { if (data.name == "Indexes") { blendshape.indices = FBXSerializer::getIntVector(data); } else if (data.name == "Vertices") { blendshape.vertices = FBXSerializer::createVec3Vector(FBXSerializer::getDoubleVector(data)); } else if (data.name == "Normals") { blendshape.normals = FBXSerializer::createVec3Vector(FBXSerializer::getDoubleVector(data)); } } return blendshape; } QVector<int> getIndices(const QVector<QString> ids, QVector<QString> modelIDs) { QVector<int> indices; foreach (const QString& id, ids) { int index = modelIDs.indexOf(id); if (index != -1) { indices.append(index); } } return indices; } typedef QPair<int, float> WeightedIndex; void addBlendshapes(const ExtractedBlendshape& extracted, const QList<WeightedIndex>& indices, ExtractedMesh& extractedMesh) { foreach (const WeightedIndex& index, indices) { extractedMesh.mesh.blendshapes.resize(max(extractedMesh.mesh.blendshapes.size(), index.first + 1)); extractedMesh.blendshapeIndexMaps.resize(extractedMesh.mesh.blendshapes.size()); HFMBlendshape& blendshape = extractedMesh.mesh.blendshapes[index.first]; QHash<int, int>& blendshapeIndexMap = extractedMesh.blendshapeIndexMaps[index.first]; for (int i = 0; i < extracted.blendshape.indices.size(); i++) { int oldIndex = extracted.blendshape.indices.at(i); for (QMultiHash<int, int>::const_iterator it = extractedMesh.newIndices.constFind(oldIndex); it != extractedMesh.newIndices.constEnd() && it.key() == oldIndex; it++) { QHash<int, int>::iterator blendshapeIndex = blendshapeIndexMap.find(it.value()); if (blendshapeIndex == blendshapeIndexMap.end()) { blendshapeIndexMap.insert(it.value(), blendshape.indices.size()); blendshape.indices.append(it.value()); blendshape.vertices.append(extracted.blendshape.vertices.at(i) * index.second); blendshape.normals.append(extracted.blendshape.normals.at(i) * index.second); } else { blendshape.vertices[*blendshapeIndex] += extracted.blendshape.vertices.at(i) * index.second; blendshape.normals[*blendshapeIndex] += extracted.blendshape.normals.at(i) * index.second; } } } } } QString getTopModelID(const QMultiMap<QString, QString>& connectionParentMap, const QHash<QString, FBXModel>& fbxModels, const QString& modelID, const QString& url) { QString topID = modelID; QVector<QString> visitedNodes; // Used to prevent following a cycle forever { visitedNodes.append(topID); // Append each node we visit foreach (const QString& parentID, connectionParentMap.values(topID)) { if (visitedNodes.contains(parentID)) { qCWarning(modelformat) << "Ignoring loop detected in FBX connection map for" << url; continue; } if (fbxModels.contains(parentID)) { topID = parentID; goto outerContinue; } } return topID; outerContinue: ; } } QString getString(const QVariant& value) { // if it's a list, return the first entry QVariantList list = value.toList(); return list.isEmpty() ? value.toString() : list.at(0).toString(); } class AnimationCurve { public: QVector<float> values; }; bool checkMaterialsHaveTextures(const QHash<QString, HFMMaterial>& materials, const QHash<QString, hifi::ByteArray>& textureFilenames, const QMultiMap<QString, QString>& _connectionChildMap) { foreach (const QString& materialID, materials.keys()) { foreach (const QString& childID, _connectionChildMap.values(materialID)) { if (textureFilenames.contains(childID)) { return true; } } } return false; } int matchTextureUVSetToAttributeChannel(const QString& texUVSetName, const QHash<QString, int>& texcoordChannels) { if (texUVSetName.isEmpty()) { return 0; } else { QHash<QString, int>::const_iterator tcUnit = texcoordChannels.find(texUVSetName); if (tcUnit != texcoordChannels.end()) { int channel = (*tcUnit); if (channel >= 2) { channel = 0; } return channel; } else { return 0; } } } HFMLight extractLight(const FBXNode& object) { HFMLight light; foreach (const FBXNode& subobject, object.children) { QString childname = QString(subobject.name); if (subobject.name == "Properties70") { foreach (const FBXNode& property, subobject.children) { int valIndex = 4; QString propName = QString(property.name); if (property.name == "P") { QString propname = property.properties.at(0).toString(); if (propname == "Intensity") { light.intensity = 0.01f * property.properties.at(valIndex).value<float>(); } else if (propname == "Color") { light.color = FBXSerializer::getVec3(property.properties, valIndex); } } } } else if ( subobject.name == "GeometryVersion" || subobject.name == "TypeFlags") { } } #if defined(DEBUG_FBXSERIALIZER) QString type = object.properties.at(0).toString(); type = object.properties.at(1).toString(); type = object.properties.at(2).toString(); foreach (const QVariant& prop, object.properties) { QString proptype = prop.typeName(); QString propval = prop.toString(); if (proptype == "Properties70") { } } #endif return light; } hifi::ByteArray fileOnUrl(const hifi::ByteArray& filepath, const QString& url) { // in order to match the behaviour when loading models from remote URLs // we assume that all external textures are right beside the loaded model // ignoring any relative paths or absolute paths inside of models return filepath.mid(filepath.lastIndexOf('/') + 1); } HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const QString& url) { const FBXNode& node = _rootNode; bool deduplicateIndices = mapping["deduplicateIndices"].toBool(); QMap<QString, ExtractedMesh> meshes; QHash<QString, QString> modelIDsToNames; QHash<QString, int> meshIDsToMeshIndices; QHash<QString, QString> ooChildToParent; QVector<ExtractedBlendshape> blendshapes; QHash<QString, FBXModel> fbxModels; QHash<QString, Cluster> clusters; QHash<QString, AnimationCurve> animationCurves; QHash<QString, QString> typeFlags; QHash<QString, QString> localRotations; QHash<QString, QString> localTranslations; QHash<QString, QString> xComponents; QHash<QString, QString> yComponents; QHash<QString, QString> zComponents; std::map<QString, HFMLight> lights; hifi::VariantMultiHash blendshapeMappings = mapping.value("bs").toHash(); QMultiHash<hifi::ByteArray, WeightedIndex> blendshapeIndices; for (int i = 0;; i++) { hifi::ByteArray blendshapeName = BLENDSHAPE_NAMES[i]; if (blendshapeName.isEmpty()) { break; } QList<QVariant> mappings = blendshapeMappings.values(blendshapeName); if (mappings.isEmpty()) { blendshapeIndices.insert(blendshapeName, WeightedIndex(i, 1.0f)); } else { foreach (const QVariant& mapping, mappings) { QVariantList blendshapeMapping = mapping.toList(); blendshapeIndices.insert(blendshapeMapping.at(0).toByteArray(), WeightedIndex(i, blendshapeMapping.at(1).toFloat())); } } } QMultiHash<QString, WeightedIndex> blendshapeChannelIndices; #if defined(DEBUG_FBXSERIALIZER) int unknown = 0; #endif HFMModel* hfmModelPtr = new HFMModel; HFMModel& hfmModel = *hfmModelPtr; hfmModel.originalURL = url; float unitScaleFactor = 1.0f; glm::quat upAxisZRotation; bool applyUpAxisZRotation = false; glm::vec3 ambientColor; QString hifiGlobalNodeID; unsigned int meshIndex = 0; haveReportedUnhandledRotationOrder = false; int fbxVersionNumber = -1; bool isBlenderVersionLower280 = false; foreach (const FBXNode& child, node.children) { if (child.name == "Creator") { // Match Blender version lower than 2.80 // Example version string from Blender 2.78: "Blender (stable FBX IO) - 2.78 (sub 0) - 3.7.7" // Example version string from Blender 2.92: "Blender (stable FBX IO) - 2.92.0 - 4.22.0" QRegularExpression blenderVersionRegex(".*Blender.*-\\s(\\d+)\\.(\\d+).*-.*"); QRegularExpressionMatch blenderVersionMatch = blenderVersionRegex.match(child.properties.at(0).toString()); if (blenderVersionMatch.hasMatch()) { int major = blenderVersionMatch.captured(1).toInt(); int minor = blenderVersionMatch.captured(2).toInt(); isBlenderVersionLower280 = major < 2 || (major == 2 && minor < 80); } } } foreach (const FBXNode& child, node.children) { if (child.name == "FBXHeaderExtension") { foreach (const FBXNode& object, child.children) { if (object.name == "SceneInfo") { foreach (const FBXNode& subobject, object.children) { if (subobject.name == "MetaData") { foreach (const FBXNode& subsubobject, subobject.children) { if (subsubobject.name == "Author") { hfmModel.author = subsubobject.properties.at(0).toString(); } } } else if (subobject.name == "Properties70") { foreach (const FBXNode& subsubobject, subobject.children) { static const QVariant APPLICATION_NAME = QVariant(hifi::ByteArray("Original|ApplicationName")); if (subsubobject.name == "P" && subsubobject.properties.size() >= 5 && subsubobject.properties.at(0) == APPLICATION_NAME) { hfmModel.applicationName = subsubobject.properties.at(4).toString(); } } } } } else if (object.name == "FBXVersion") { fbxVersionNumber = object.properties.at(0).toInt();