// // FBXReader.cpp // interface/src/renderer // // Created by Andrzej Kapolka on 9/18/13. // Copyright 2013 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "FBXReader.h" #include "ModelFormatLogging.h" // TOOL: Uncomment the following line to enable the filtering of all the unkwnon fields of a node so we can break point easily while loading a model with problems... //#define DEBUG_FBXREADER using namespace std; int FBXGeometryPointerMetaTypeId = qRegisterMetaType(); QStringList FBXGeometry::getJointNames() const { QStringList names; foreach (const FBXJoint& joint, joints) { names.append(joint.name); } return names; } bool FBXGeometry::hasBlendedMeshes() const { if (!meshes.isEmpty()) { foreach (const FBXMesh& mesh, meshes) { if (!mesh.blendshapes.isEmpty()) { return true; } } } return false; } Extents FBXGeometry::getUnscaledMeshExtents() const { const Extents& extents = meshExtents; // even though our caller asked for "unscaled" we need to include any fst scaling, translation, and rotation, which // is captured in the offset matrix glm::vec3 minimum = glm::vec3(offset * glm::vec4(extents.minimum, 1.0f)); glm::vec3 maximum = glm::vec3(offset * glm::vec4(extents.maximum, 1.0f)); Extents scaledExtents = { minimum, maximum }; return scaledExtents; } // TODO: Move to model::Mesh when Sam's ready bool FBXGeometry::convexHullContains(const glm::vec3& point) const { if (!getUnscaledMeshExtents().containsPoint(point)) { return false; } auto checkEachPrimitive = [=](FBXMesh& mesh, QVector indices, int primitiveSize) -> bool { // Check whether the point is "behind" all the primitives. int verticesSize = mesh.vertices.size(); for (int j = 0; j < indices.size() - 2; // -2 in case the vertices aren't the right size -- we access j + 2 below j += primitiveSize) { if (indices[j] < verticesSize && indices[j + 1] < verticesSize && indices[j + 2] < verticesSize && !isPointBehindTrianglesPlane(point, mesh.vertices[indices[j]], mesh.vertices[indices[j + 1]], mesh.vertices[indices[j + 2]])) { // it's not behind at least one so we bail return false; } } return true; }; // Check that the point is contained in at least one convex mesh. for (auto mesh : meshes) { bool insideMesh = true; // To be considered inside a convex mesh, // the point needs to be "behind" all the primitives respective planes. for (auto part : mesh.parts) { // run through all the triangles and quads if (!checkEachPrimitive(mesh, part.triangleIndices, 3) || !checkEachPrimitive(mesh, part.quadIndices, 4)) { // If not, the point is outside, bail for this mesh insideMesh = false; continue; } } if (insideMesh) { // It's inside this mesh, return true. return true; } } // It wasn't in any mesh, return false. return false; } QString FBXGeometry::getModelNameOfMesh(int meshIndex) const { if (meshIndicesToModelNames.contains(meshIndex)) { return meshIndicesToModelNames.value(meshIndex); } return QString(); } int fbxGeometryMetaTypeId = qRegisterMetaType(); int fbxAnimationFrameMetaTypeId = qRegisterMetaType(); int fbxAnimationFrameVectorMetaTypeId = qRegisterMetaType >(); 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; } 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 getName(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 getID(const QVariantList& properties, int index = 0) { return processID(properties.at(index).toString()); } const char* HUMANIK_JOINTS[] = { "RightHand", "RightForeArm", "RightArm", "Head", "LeftArm", "LeftForeArm", "LeftHand", "Neck", "Spine", "Hips", "RightUpLeg", "LeftUpLeg", "RightLeg", "LeftLeg", "RightFoot", "LeftFoot", "" }; 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; }; glm::mat4 getGlobalTransform(const QMultiMap& _connectionParentMap, const QHash& models, QString nodeID, bool mixamoHack, const QString& url) { glm::mat4 globalTransform; QVector visitedNodes; // Used to prevent following a cycle while (!nodeID.isNull()) { visitedNodes.append(nodeID); // Append each node we visit const FBXModel& model = models.value(nodeID); globalTransform = glm::translate(model.translation) * model.preTransform * glm::mat4_cast(model.preRotation * model.rotation * model.postRotation) * model.postTransform * globalTransform; if (mixamoHack) { // there's something weird about the models from Mixamo Fuse; they don't skin right with the full transform return globalTransform; } QList 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 (models.contains(parentID)) { nodeID = parentID; break; } } } return globalTransform; } class ExtractedBlendshape { public: QString id; FBXBlendshape blendshape; }; void printNode(const FBXNode& node, int indentLevel) { int indentLength = 2; QByteArray 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 indices; QVector weights; glm::mat4 transformLink; }; void appendModelIDs(const QString& parentID, const QMultiMap& connectionChildMap, QHash& models, QSet& remainingModels, QVector& modelIDs) { if (remainingModels.contains(parentID)) { modelIDs.append(parentID); remainingModels.remove(parentID); } int parentIndex = modelIDs.size() - 1; foreach (const QString& childID, connectionChildMap.values(parentID)) { if (remainingModels.contains(childID)) { FBXModel& model = models[childID]; if (model.parentIndex == -1) { model.parentIndex = parentIndex; appendModelIDs(childID, connectionChildMap, models, remainingModels, modelIDs); } } } } FBXBlendshape extractBlendshape(const FBXNode& object) { FBXBlendshape blendshape; foreach (const FBXNode& data, object.children) { if (data.name == "Indexes") { blendshape.indices = FBXReader::getIntVector(data); } else if (data.name == "Vertices") { blendshape.vertices = FBXReader::createVec3Vector(FBXReader::getDoubleVector(data)); } else if (data.name == "Normals") { blendshape.normals = FBXReader::createVec3Vector(FBXReader::getDoubleVector(data)); } } return blendshape; } void setTangents(FBXMesh& mesh, int firstIndex, int secondIndex) { const glm::vec3& normal = mesh.normals.at(firstIndex); glm::vec3 bitangent = glm::cross(normal, mesh.vertices.at(secondIndex) - mesh.vertices.at(firstIndex)); if (glm::length(bitangent) < EPSILON) { return; } glm::vec2 texCoordDelta = mesh.texCoords.at(secondIndex) - mesh.texCoords.at(firstIndex); glm::vec3 normalizedNormal = glm::normalize(normal); mesh.tangents[firstIndex] += glm::cross(glm::angleAxis(-atan2f(-texCoordDelta.t, texCoordDelta.s), normalizedNormal) * glm::normalize(bitangent), normalizedNormal); } QVector getIndices(const QVector ids, QVector modelIDs) { QVector indices; foreach (const QString& id, ids) { int index = modelIDs.indexOf(id); if (index != -1) { indices.append(index); } } return indices; } typedef QPair WeightedIndex; void addBlendshapes(const ExtractedBlendshape& extracted, const QList& 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()); FBXBlendshape& blendshape = extractedMesh.mesh.blendshapes[index.first]; QHash& blendshapeIndexMap = extractedMesh.blendshapeIndexMaps[index.first]; for (int i = 0; i < extracted.blendshape.indices.size(); i++) { int oldIndex = extracted.blendshape.indices.at(i); for (QMultiHash::const_iterator it = extractedMesh.newIndices.constFind(oldIndex); it != extractedMesh.newIndices.constEnd() && it.key() == oldIndex; it++) { QHash::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& connectionParentMap, const QHash& models, const QString& modelID, const QString& url) { QString topID = modelID; QVector 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 (models.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(); } typedef std::vector ShapeVertices; class AnimationCurve { public: QVector values; }; bool checkMaterialsHaveTextures(const QHash& materials, const QHash& textureFilenames, const QMultiMap& _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& texcoordChannels) { if (texUVSetName.isEmpty()) { return 0; } else { QHash::const_iterator tcUnit = texcoordChannels.find(texUVSetName); if (tcUnit != texcoordChannels.end()) { int channel = (*tcUnit); if (channel >= 2) { channel = 0; } return channel; } else { return 0; } } } FBXLight extractLight(const FBXNode& object) { FBXLight 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(); } else if (propname == "Color") { light.color = FBXReader::getVec3(property.properties, valIndex); } } } } else if ( subobject.name == "GeometryVersion" || subobject.name == "TypeFlags") { } } #if defined(DEBUG_FBXREADER) 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; } QByteArray fileOnUrl(const QByteArray& 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); } FBXGeometry* FBXReader::extractFBXGeometry(const QVariantHash& mapping, const QString& url) { const FBXNode& node = _fbxNode; QMap meshes; QHash modelIDsToNames; QHash meshIDsToMeshIndices; QHash ooChildToParent; QVector blendshapes; QHash models; QHash clusters; QHash animationCurves; QHash typeFlags; QHash localRotations; QHash localTranslations; QHash xComponents; QHash yComponents; QHash zComponents; std::map lights; QVariantHash joints = mapping.value("joint").toHash(); QString jointEyeLeftName = processID(getString(joints.value("jointEyeLeft", "jointEyeLeft"))); QString jointEyeRightName = processID(getString(joints.value("jointEyeRight", "jointEyeRight"))); QString jointNeckName = processID(getString(joints.value("jointNeck", "jointNeck"))); QString jointRootName = processID(getString(joints.value("jointRoot", "jointRoot"))); QString jointLeanName = processID(getString(joints.value("jointLean", "jointLean"))); QString jointHeadName = processID(getString(joints.value("jointHead", "jointHead"))); QString jointLeftHandName = processID(getString(joints.value("jointLeftHand", "jointLeftHand"))); QString jointRightHandName = processID(getString(joints.value("jointRightHand", "jointRightHand"))); QString jointEyeLeftID; QString jointEyeRightID; QString jointNeckID; QString jointRootID; QString jointLeanID; QString jointHeadID; QString jointLeftHandID; QString jointRightHandID; QString jointLeftToeID; QString jointRightToeID; QVector humanIKJointNames; for (int i = 0;; i++) { QByteArray jointName = HUMANIK_JOINTS[i]; if (jointName.isEmpty()) { break; } humanIKJointNames.append(processID(getString(joints.value(jointName, jointName)))); } QVector humanIKJointIDs(humanIKJointNames.size()); QVariantHash blendshapeMappings = mapping.value("bs").toHash(); QMultiHash blendshapeIndices; for (int i = 0;; i++) { QByteArray blendshapeName = FACESHIFT_BLENDSHAPES[i]; if (blendshapeName.isEmpty()) { break; } QList 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 blendshapeChannelIndices; #if defined(DEBUG_FBXREADER) int unknown = 0; #endif FBXGeometry* geometryPtr = new FBXGeometry; FBXGeometry& geometry = *geometryPtr; geometry.originalURL = url; float unitScaleFactor = 1.0f; glm::vec3 ambientColor; QString hifiGlobalNodeID; unsigned int meshIndex = 0; 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") { geometry.author = subsubobject.properties.at(0).toString(); } } } else if (subobject.name == "Properties70") { foreach (const FBXNode& subsubobject, subobject.children) { static const QVariant APPLICATION_NAME = QVariant(QByteArray("Original|ApplicationName")); if (subsubobject.name == "P" && subsubobject.properties.size() >= 5 && subsubobject.properties.at(0) == APPLICATION_NAME) { geometry.applicationName = subsubobject.properties.at(4).toString(); } } } } } } } else if (child.name == "GlobalSettings") { foreach (const FBXNode& object, child.children) { if (object.name == "Properties70") { QString propertyName = "P"; int index = 4; foreach (const FBXNode& subobject, object.children) { if (subobject.name == propertyName) { static const QVariant UNIT_SCALE_FACTOR = QByteArray("UnitScaleFactor"); static const QVariant AMBIENT_COLOR = QByteArray("AmbientColor"); const auto& subpropName = subobject.properties.at(0); if (subpropName == UNIT_SCALE_FACTOR) { unitScaleFactor = subobject.properties.at(index).toFloat(); } else if (subpropName == AMBIENT_COLOR) { ambientColor = getVec3(subobject.properties, index); } } } } } } else if (child.name == "Objects") { foreach (const FBXNode& object, child.children) { if (object.name == "Geometry") { if (object.properties.at(2) == "Mesh") { meshes.insert(getID(object.properties), extractMesh(object, meshIndex)); } else { // object.properties.at(2) == "Shape" ExtractedBlendshape extracted = { getID(object.properties), extractBlendshape(object) }; blendshapes.append(extracted); } } else if (object.name == "Model") { QString name = getName(object.properties); QString id = getID(object.properties); modelIDsToNames.insert(id, name); QString modelname = name.toLower(); if (modelname.startsWith("hifi")) { hifiGlobalNodeID = id; } if (name == jointEyeLeftName || name == "EyeL" || name == "joint_Leye") { jointEyeLeftID = getID(object.properties); } else if (name == jointEyeRightName || name == "EyeR" || name == "joint_Reye") { jointEyeRightID = getID(object.properties); } else if (name == jointNeckName || name == "NeckRot" || name == "joint_neck") { jointNeckID = getID(object.properties); } else if (name == jointRootName) { jointRootID = getID(object.properties); } else if (name == jointLeanName) { jointLeanID = getID(object.properties); } else if (name == jointHeadName) { jointHeadID = getID(object.properties); } else if (name == jointLeftHandName || name == "LeftHand" || name == "joint_L_hand") { jointLeftHandID = getID(object.properties); } else if (name == jointRightHandName || name == "RightHand" || name == "joint_R_hand") { jointRightHandID = getID(object.properties); } else if (name == "LeftToe" || name == "joint_L_toe" || name == "LeftToe_End") { jointLeftToeID = getID(object.properties); } else if (name == "RightToe" || name == "joint_R_toe" || name == "RightToe_End") { jointRightToeID = getID(object.properties); } int humanIKJointIndex = humanIKJointNames.indexOf(name); if (humanIKJointIndex != -1) { humanIKJointIDs[humanIKJointIndex] = getID(object.properties); } glm::vec3 translation; // NOTE: the euler angles as supplied by the FBX file are in degrees glm::vec3 rotationOffset; glm::vec3 preRotation, rotation, postRotation; glm::vec3 scale = glm::vec3(1.0f, 1.0f, 1.0f); glm::vec3 scalePivot, rotationPivot, scaleOffset; bool rotationMinX = false, rotationMinY = false, rotationMinZ = false; bool rotationMaxX = false, rotationMaxY = false, rotationMaxZ = false; // local offset transforms from 3ds max bool hasGeometricOffset = false; glm::vec3 geometricTranslation; glm::vec3 geometricScaling(1.0f, 1.0f, 1.0f); glm::vec3 geometricRotation; glm::vec3 rotationMin, rotationMax; FBXModel model = { name, -1, glm::vec3(), glm::mat4(), glm::quat(), glm::quat(), glm::quat(), glm::mat4(), glm::vec3(), glm::vec3(), false, glm::vec3(), glm::quat(), glm::vec3(1.0f) }; ExtractedMesh* mesh = NULL; QVector blendshapes; foreach (const FBXNode& subobject, object.children) { bool properties = false; QByteArray propertyName; int index; if (subobject.name == "Properties60") { properties = true; propertyName = "Property"; index = 3; } else if (subobject.name == "Properties70") { properties = true; propertyName = "P"; index = 4; } if (properties) { static const QVariant GEOMETRIC_TRANSLATION = QByteArray("GeometricTranslation"); static const QVariant GEOMETRIC_ROTATION = QByteArray("GeometricRotation"); static const QVariant GEOMETRIC_SCALING = QByteArray("GeometricScaling"); static const QVariant LCL_TRANSLATION = QByteArray("Lcl Translation"); static const QVariant LCL_ROTATION = QByteArray("Lcl Rotation"); static const QVariant LCL_SCALING = QByteArray("Lcl Scaling"); static const QVariant ROTATION_MAX = QByteArray("RotationMax"); static const QVariant ROTATION_MAX_X = QByteArray("RotationMaxX"); static const QVariant ROTATION_MAX_Y = QByteArray("RotationMaxY"); static const QVariant ROTATION_MAX_Z = QByteArray("RotationMaxZ"); static const QVariant ROTATION_MIN = QByteArray("RotationMin"); static const QVariant ROTATION_MIN_X = QByteArray("RotationMinX"); static const QVariant ROTATION_MIN_Y = QByteArray("RotationMinY"); static const QVariant ROTATION_MIN_Z = QByteArray("RotationMinZ"); static const QVariant ROTATION_OFFSET = QByteArray("RotationOffset"); static const QVariant ROTATION_PIVOT = QByteArray("RotationPivot"); static const QVariant SCALING_OFFSET = QByteArray("ScalingOffset"); static const QVariant SCALING_PIVOT = QByteArray("ScalingPivot"); static const QVariant PRE_ROTATION = QByteArray("PreRotation"); static const QVariant POST_ROTATION = QByteArray("PostRotation"); foreach(const FBXNode& property, subobject.children) { const auto& childProperty = property.properties.at(0); if (property.name == propertyName) { if (childProperty == LCL_TRANSLATION) { translation = getVec3(property.properties, index); } else if (childProperty == ROTATION_OFFSET) { rotationOffset = getVec3(property.properties, index); } else if (childProperty == ROTATION_PIVOT) { rotationPivot = getVec3(property.properties, index); } else if (childProperty == PRE_ROTATION) { preRotation = getVec3(property.properties, index); } else if (childProperty == LCL_ROTATION) { rotation = getVec3(property.properties, index); } else if (childProperty == POST_ROTATION) { postRotation = getVec3(property.properties, index); } else if (childProperty == SCALING_PIVOT) { scalePivot = getVec3(property.properties, index); } else if (childProperty == LCL_SCALING) { scale = getVec3(property.properties, index); } else if (childProperty == SCALING_OFFSET) { scaleOffset = getVec3(property.properties, index); // NOTE: these rotation limits are stored in degrees (NOT radians) } else if (childProperty == ROTATION_MIN) { rotationMin = getVec3(property.properties, index); } else if (childProperty == ROTATION_MAX) { rotationMax = getVec3(property.properties, index); } else if (childProperty == ROTATION_MIN_X) { rotationMinX = property.properties.at(index).toBool(); } else if (childProperty == ROTATION_MIN_Y) { rotationMinY = property.properties.at(index).toBool(); } else if (childProperty == ROTATION_MIN_Z) { rotationMinZ = property.properties.at(index).toBool(); } else if (childProperty == ROTATION_MAX_X) { rotationMaxX = property.properties.at(index).toBool(); } else if (childProperty == ROTATION_MAX_Y) { rotationMaxY = property.properties.at(index).toBool(); } else if (childProperty == ROTATION_MAX_Z) { rotationMaxZ = property.properties.at(index).toBool(); } else if (childProperty == GEOMETRIC_TRANSLATION) { geometricTranslation = getVec3(property.properties, index); hasGeometricOffset = true; } else if (childProperty == GEOMETRIC_ROTATION) { geometricRotation = getVec3(property.properties, index); hasGeometricOffset = true; } else if (childProperty == GEOMETRIC_SCALING) { geometricScaling = getVec3(property.properties, index); hasGeometricOffset = true; } } } } else if (subobject.name == "Vertices") { // it's a mesh as well as a model mesh = &meshes[getID(object.properties)]; *mesh = extractMesh(object, meshIndex); } else if (subobject.name == "Shape") { ExtractedBlendshape blendshape = { subobject.properties.at(0).toString(), extractBlendshape(subobject) }; blendshapes.append(blendshape); } #if defined(DEBUG_FBXREADER) else if (subobject.name == "TypeFlags") { QString attributetype = subobject.properties.at(0).toString(); if (!attributetype.empty()) { if (attributetype == "Light") { QString lightprop; foreach (const QVariant& vprop, subobject.properties) { lightprop = vprop.toString(); } FBXLight light = extractLight(object); } } } else { QString whatisthat = subobject.name; if (whatisthat == "Shape") { } } #endif } // add the blendshapes included in the model, if any if (mesh) { foreach (const ExtractedBlendshape& extracted, blendshapes) { addBlendshapes(extracted, blendshapeIndices.values(extracted.id.toLatin1()), *mesh); } } // see FBX documentation, http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/index.html model.translation = translation; model.preTransform = glm::translate(rotationOffset) * glm::translate(rotationPivot); model.preRotation = glm::quat(glm::radians(preRotation)); model.rotation = glm::quat(glm::radians(rotation)); model.postRotation = glm::inverse(glm::quat(glm::radians(postRotation))); model.postTransform = glm::translate(-rotationPivot) * glm::translate(scaleOffset) * glm::translate(scalePivot) * glm::scale(scale) * glm::translate(-scalePivot); // NOTE: angles from the FBX file are in degrees // so we convert them to radians for the FBXModel class model.rotationMin = glm::radians(glm::vec3(rotationMinX ? rotationMin.x : -180.0f, rotationMinY ? rotationMin.y : -180.0f, rotationMinZ ? rotationMin.z : -180.0f)); model.rotationMax = glm::radians(glm::vec3(rotationMaxX ? rotationMax.x : 180.0f, rotationMaxY ? rotationMax.y : 180.0f, rotationMaxZ ? rotationMax.z : 180.0f)); model.hasGeometricOffset = hasGeometricOffset; model.geometricTranslation = geometricTranslation; model.geometricRotation = glm::quat(glm::radians(geometricRotation)); model.geometricScaling = geometricScaling; models.insert(getID(object.properties), model); } else if (object.name == "Texture") { TextureParam tex; foreach (const FBXNode& subobject, object.children) { const int RELATIVE_FILENAME_MIN_SIZE = 1; const int TEXTURE_NAME_MIN_SIZE = 1; const int TEXTURE_ALPHA_SOURCE_MIN_SIZE = 1; const int MODEL_UV_TRANSLATION_MIN_SIZE = 2; const int MODEL_UV_SCALING_MIN_SIZE = 2; const int CROPPING_MIN_SIZE = 4; if (subobject.name == "RelativeFilename" && subobject.properties.length() >= RELATIVE_FILENAME_MIN_SIZE) { QByteArray filename = subobject.properties.at(0).toByteArray(); QByteArray filepath = filename.replace('\\', '/'); filename = fileOnUrl(filepath, url); _textureFilepaths.insert(getID(object.properties), filepath); _textureFilenames.insert(getID(object.properties), filename); } else if (subobject.name == "TextureName" && subobject.properties.length() >= TEXTURE_NAME_MIN_SIZE) { // trim the name from the timestamp QString name = QString(subobject.properties.at(0).toByteArray()); name = name.left(name.indexOf('[')); _textureNames.insert(getID(object.properties), name); } else if (subobject.name == "Texture_Alpha_Source" && subobject.properties.length() >= TEXTURE_ALPHA_SOURCE_MIN_SIZE) { tex.assign(tex.alphaSource, subobject.properties.at(0).value()); } else if (subobject.name == "ModelUVTranslation" && subobject.properties.length() >= MODEL_UV_TRANSLATION_MIN_SIZE) { tex.assign(tex.UVTranslation, glm::vec2(subobject.properties.at(0).value(), subobject.properties.at(1).value())); } else if (subobject.name == "ModelUVScaling" && subobject.properties.length() >= MODEL_UV_SCALING_MIN_SIZE) { tex.assign(tex.UVScaling, glm::vec2(subobject.properties.at(0).value(), subobject.properties.at(1).value())); if (tex.UVScaling.x == 0.0f) { tex.UVScaling.x = 1.0f; } if (tex.UVScaling.y == 0.0f) { tex.UVScaling.y = 1.0f; } } else if (subobject.name == "Cropping" && subobject.properties.length() >= CROPPING_MIN_SIZE) { tex.assign(tex.cropping, glm::vec4(subobject.properties.at(0).value(), subobject.properties.at(1).value(), subobject.properties.at(2).value(), subobject.properties.at(3).value())); } else if (subobject.name == "Properties70") { QByteArray propertyName; int index; propertyName = "P"; index = 4; foreach (const FBXNode& property, subobject.children) { static const QVariant UV_SET = QByteArray("UVSet"); static const QVariant CURRENT_TEXTURE_BLEND_MODE = QByteArray("CurrentTextureBlendMode"); static const QVariant USE_MATERIAL = QByteArray("UseMaterial"); static const QVariant TRANSLATION = QByteArray("Translation"); static const QVariant ROTATION = QByteArray("Rotation"); static const QVariant SCALING = QByteArray("Scaling"); if (property.name == propertyName) { QString v = property.properties.at(0).toString(); if (property.properties.at(0) == UV_SET) { std::string uvName = property.properties.at(index).toString().toStdString(); tex.assign(tex.UVSet, property.properties.at(index).toString()); } else if (property.properties.at(0) == CURRENT_TEXTURE_BLEND_MODE) { tex.assign(tex.currentTextureBlendMode, property.properties.at(index).value()); } else if (property.properties.at(0) == USE_MATERIAL) { tex.assign(tex.useMaterial, property.properties.at(index).value()); } else if (property.properties.at(0) == TRANSLATION) { tex.assign(tex.translation, getVec3(property.properties, index)); } else if (property.properties.at(0) == ROTATION) { tex.assign(tex.rotation, getVec3(property.properties, index)); } else if (property.properties.at(0) == SCALING) { tex.assign(tex.scaling, getVec3(property.properties, index)); if (tex.scaling.x == 0.0f) { tex.scaling.x = 1.0f; } if (tex.scaling.y == 0.0f) { tex.scaling.y = 1.0f; } if (tex.scaling.z == 0.0f) { tex.scaling.z = 1.0f; } } #if defined(DEBUG_FBXREADER) else { QString propName = v; unknown++; } #endif } } } #if defined(DEBUG_FBXREADER) else { if (subobject.name == "Type") { } else if (subobject.name == "Version") { } else if (subobject.name == "FileName") { } else if (subobject.name == "Media") { } else { QString subname = subobject.name.data(); unknown++; } } #endif } if (!tex.isDefault) { _textureParams.insert(getID(object.properties), tex); } } else if (object.name == "Video") { QByteArray filepath; QByteArray content; foreach (const FBXNode& subobject, object.children) { if (subobject.name == "RelativeFilename") { filepath= subobject.properties.at(0).toByteArray(); filepath = filepath.replace('\\', '/'); } else if (subobject.name == "Content" && !subobject.properties.isEmpty()) { content = subobject.properties.at(0).toByteArray(); } } if (!content.isEmpty()) { _textureContent.insert(filepath, content); } } else if (object.name == "Material") { FBXMaterial material; material.name = (object.properties.at(1).toString()); foreach (const FBXNode& subobject, object.children) { bool properties = false; QByteArray propertyName; int index; if (subobject.name == "Properties60") { properties = true; propertyName = "Property"; index = 3; } else if (subobject.name == "Properties70") { properties = true; propertyName = "P"; index = 4; } else if (subobject.name == "ShadingModel") { material.shadingModel = subobject.properties.at(0).toString(); } if (properties) { std::vector unknowns; static const QVariant DIFFUSE_COLOR = QByteArray("DiffuseColor"); static const QVariant DIFFUSE_FACTOR = QByteArray("DiffuseFactor"); static const QVariant DIFFUSE = QByteArray("Diffuse"); static const QVariant SPECULAR_COLOR = QByteArray("SpecularColor"); static const QVariant SPECULAR_FACTOR = QByteArray("SpecularFactor"); static const QVariant SPECULAR = QByteArray("Specular"); static const QVariant EMISSIVE_COLOR = QByteArray("EmissiveColor"); static const QVariant EMISSIVE_FACTOR = QByteArray("EmissiveFactor"); static const QVariant EMISSIVE = QByteArray("Emissive"); static const QVariant AMBIENT_FACTOR = QByteArray("AmbientFactor"); static const QVariant SHININESS = QByteArray("Shininess"); static const QVariant OPACITY = QByteArray("Opacity"); static const QVariant MAYA_USE_NORMAL_MAP = QByteArray("Maya|use_normal_map"); static const QVariant MAYA_BASE_COLOR = QByteArray("Maya|base_color"); static const QVariant MAYA_USE_COLOR_MAP = QByteArray("Maya|use_color_map"); static const QVariant MAYA_ROUGHNESS = QByteArray("Maya|roughness"); static const QVariant MAYA_USE_ROUGHNESS_MAP = QByteArray("Maya|use_roughness_map"); static const QVariant MAYA_METALLIC = QByteArray("Maya|metallic"); static const QVariant MAYA_USE_METALLIC_MAP = QByteArray("Maya|use_metallic_map"); static const QVariant MAYA_EMISSIVE = QByteArray("Maya|emissive"); static const QVariant MAYA_EMISSIVE_INTENSITY = QByteArray("Maya|emissive_intensity"); static const QVariant MAYA_USE_EMISSIVE_MAP = QByteArray("Maya|use_emissive_map"); static const QVariant MAYA_USE_AO_MAP = QByteArray("Maya|use_ao_map"); foreach(const FBXNode& property, subobject.children) { if (property.name == propertyName) { if (property.properties.at(0) == DIFFUSE_COLOR) { material.diffuseColor = getVec3(property.properties, index); } else if (property.properties.at(0) == DIFFUSE_FACTOR) { material.diffuseFactor = property.properties.at(index).value(); } else if (property.properties.at(0) == DIFFUSE) { // NOTE: this is uneeded but keep it for now for debug // material.diffuseColor = getVec3(property.properties, index); // material.diffuseFactor = 1.0; } else if (property.properties.at(0) == SPECULAR_COLOR) { material.specularColor = getVec3(property.properties, index); } else if (property.properties.at(0) == SPECULAR_FACTOR) { material.specularFactor = property.properties.at(index).value(); } else if (property.properties.at(0) == SPECULAR) { // NOTE: this is uneeded but keep it for now for debug // material.specularColor = getVec3(property.properties, index); // material.specularFactor = 1.0; } else if (property.properties.at(0) == EMISSIVE_COLOR) { material.emissiveColor = getVec3(property.properties, index); } else if (property.properties.at(0) == EMISSIVE_FACTOR) { material.emissiveFactor = property.properties.at(index).value(); } else if (property.properties.at(0) == EMISSIVE) { // NOTE: this is uneeded but keep it for now for debug // material.emissiveColor = getVec3(property.properties, index); // material.emissiveFactor = 1.0; } else if (property.properties.at(0) == AMBIENT_FACTOR) { material.ambientFactor = property.properties.at(index).value(); // Detected just for BLender AO vs lightmap } else if (property.properties.at(0) == SHININESS) { material.shininess = property.properties.at(index).value(); } else if (property.properties.at(0) == OPACITY) { material.opacity = property.properties.at(index).value(); } // Sting Ray Material Properties!!!! else if (property.properties.at(0) == MAYA_USE_NORMAL_MAP) { material.isPBSMaterial = true; material.useNormalMap = (bool)property.properties.at(index).value(); } else if (property.properties.at(0) == MAYA_BASE_COLOR) { material.isPBSMaterial = true; material.diffuseColor = getVec3(property.properties, index); } else if (property.properties.at(0) == MAYA_USE_COLOR_MAP) { material.isPBSMaterial = true; material.useAlbedoMap = (bool) property.properties.at(index).value(); } else if (property.properties.at(0) == MAYA_ROUGHNESS) { material.isPBSMaterial = true; material.roughness = property.properties.at(index).value(); } else if (property.properties.at(0) == MAYA_USE_ROUGHNESS_MAP) { material.isPBSMaterial = true; material.useRoughnessMap = (bool)property.properties.at(index).value(); } else if (property.properties.at(0) == MAYA_METALLIC) { material.isPBSMaterial = true; material.metallic = property.properties.at(index).value(); } else if (property.properties.at(0) == MAYA_USE_METALLIC_MAP) { material.isPBSMaterial = true; material.useMetallicMap = (bool)property.properties.at(index).value(); } else if (property.properties.at(0) == MAYA_EMISSIVE) { material.isPBSMaterial = true; material.emissiveColor = getVec3(property.properties, index); } else if (property.properties.at(0) == MAYA_EMISSIVE_INTENSITY) { material.isPBSMaterial = true; material.emissiveIntensity = property.properties.at(index).value(); } else if (property.properties.at(0) == MAYA_USE_EMISSIVE_MAP) { material.isPBSMaterial = true; material.useEmissiveMap = (bool)property.properties.at(index).value(); } else if (property.properties.at(0) == MAYA_USE_AO_MAP) { material.isPBSMaterial = true; material.useOcclusionMap = (bool)property.properties.at(index).value(); } else { const QString propname = property.properties.at(0).toString(); unknowns.push_back(propname.toStdString()); } } } } #if defined(DEBUG_FBXREADER) else { QString propname = subobject.name.data(); int unknown = 0; if ( (propname == "Version") ||(propname == "Multilayer")) { } else { unknown++; } } #endif } material.materialID = getID(object.properties); _fbxMaterials.insert(material.materialID, material); } else if (object.name == "NodeAttribute") { #if defined(DEBUG_FBXREADER) std::vector properties; foreach(const QVariant& v, object.properties) { properties.push_back(v.toString()); } #endif QString attribID = getID(object.properties); QString attributetype; foreach (const FBXNode& subobject, object.children) { if (subobject.name == "TypeFlags") { typeFlags.insert(getID(object.properties), subobject.properties.at(0).toString()); attributetype = subobject.properties.at(0).toString(); } } if (!attributetype.isEmpty()) { if (attributetype == "Light") { FBXLight light = extractLight(object); lights[attribID] = light; } } } else if (object.name == "Deformer") { if (object.properties.last() == "Cluster") { Cluster cluster; foreach (const FBXNode& subobject, object.children) { if (subobject.name == "Indexes") { cluster.indices = getIntVector(subobject); } else if (subobject.name == "Weights") { cluster.weights = getDoubleVector(subobject); } else if (subobject.name == "TransformLink") { QVector values = getDoubleVector(subobject); cluster.transformLink = createMat4(values); } } clusters.insert(getID(object.properties), cluster); } else if (object.properties.last() == "BlendShapeChannel") { QByteArray name = object.properties.at(1).toByteArray(); name = name.left(name.indexOf('\0')); if (!blendshapeIndices.contains(name)) { // try everything after the dot name = name.mid(name.lastIndexOf('.') + 1); } QString id = getID(object.properties); geometry.blendshapeChannelNames << name; foreach (const WeightedIndex& index, blendshapeIndices.values(name)) { blendshapeChannelIndices.insert(id, index); } } } else if (object.name == "AnimationCurve") { AnimationCurve curve; foreach (const FBXNode& subobject, object.children) { if (subobject.name == "KeyValueFloat") { curve.values = getFloatVector(subobject); } } animationCurves.insert(getID(object.properties), curve); } #if defined(DEBUG_FBXREADER) else { QString objectname = object.name.data(); if ( objectname == "Pose" || objectname == "AnimationStack" || objectname == "AnimationLayer" || objectname == "AnimationCurveNode") { } else { unknown++; } } #endif } } else if (child.name == "Connections") { static const QVariant OO = QByteArray("OO"); static const QVariant OP = QByteArray("OP"); foreach (const FBXNode& connection, child.children) { if (connection.name == "C" || connection.name == "Connect") { if (connection.properties.at(0) == OO) { QString childID = getID(connection.properties, 1); QString parentID = getID(connection.properties, 2); ooChildToParent.insert(childID, parentID); if (!hifiGlobalNodeID.isEmpty() && (parentID == hifiGlobalNodeID)) { std::map< QString, FBXLight >::iterator lightIt = lights.find(childID); if (lightIt != lights.end()) { _lightmapLevel = (*lightIt).second.intensity; if (_lightmapLevel <= 0.0f) { _loadLightmaps = false; } _lightmapOffset = glm::clamp((*lightIt).second.color.x, 0.f, 1.f); } } } else if (connection.properties.at(0) == OP) { int counter = 0; QByteArray type = connection.properties.at(3).toByteArray().toLower(); if (type.contains("DiffuseFactor")) { diffuseFactorTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if ((type.contains("diffuse") && !type.contains("tex_global_diffuse"))) { diffuseTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("tex_color_map")) { diffuseTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("transparentcolor")) { // Maya way of passing TransparentMap transparentTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("transparencyfactor")) { // Blender way of passing TransparentMap transparentTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("bump")) { bumpTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("normal")) { normalTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("tex_normal_map")) { normalTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if ((type.contains("specular") && !type.contains("tex_global_specular")) || type.contains("reflection")) { specularTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("tex_metallic_map")) { metallicTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("shininess")) { shininessTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("tex_roughness_map")) { roughnessTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("emissive")) { emissiveTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("tex_emissive_map")) { emissiveTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("ambientcolor")) { ambientTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("ambientfactor")) { ambientFactorTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type.contains("tex_ao_map")) { occlusionTextures.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type == "lcl rotation") { localRotations.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type == "lcl translation") { localTranslations.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type == "d|x") { xComponents.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type == "d|y") { yComponents.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else if (type == "d|z") { zComponents.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else { QString typenam = type.data(); counter++; } } _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } } } #if defined(DEBUG_FBXREADER) else { QString objectname = child.name.data(); if ( objectname == "Pose" || objectname == "CreationTime" || objectname == "FileId" || objectname == "Creator" || objectname == "Documents" || objectname == "References" || objectname == "Definitions" || objectname == "Takes" || objectname == "AnimationStack" || objectname == "AnimationLayer" || objectname == "AnimationCurveNode") { } else { unknown++; } } #endif } // TODO: check if is code is needed if (!lights.empty()) { if (hifiGlobalNodeID.isEmpty()) { auto light = lights.begin(); _lightmapLevel = (*light).second.intensity; } } // assign the blendshapes to their corresponding meshes foreach (const ExtractedBlendshape& extracted, blendshapes) { QString blendshapeChannelID = _connectionParentMap.value(extracted.id); QString blendshapeID = _connectionParentMap.value(blendshapeChannelID); QString meshID = _connectionParentMap.value(blendshapeID); addBlendshapes(extracted, blendshapeChannelIndices.values(blendshapeChannelID), meshes[meshID]); } // get offset transform from mapping float offsetScale = mapping.value("scale", 1.0f).toFloat() * unitScaleFactor * METERS_PER_CENTIMETER; glm::quat offsetRotation = glm::quat(glm::radians(glm::vec3(mapping.value("rx").toFloat(), mapping.value("ry").toFloat(), mapping.value("rz").toFloat()))); geometry.offset = glm::translate(glm::vec3(mapping.value("tx").toFloat(), mapping.value("ty").toFloat(), mapping.value("tz").toFloat())) * glm::mat4_cast(offsetRotation) * glm::scale(glm::vec3(offsetScale, offsetScale, offsetScale)); // get the list of models in depth-first traversal order QVector modelIDs; QSet remainingModels; for (QHash::const_iterator model = models.constBegin(); model != models.constEnd(); model++) { // models with clusters must be parented to the cluster top foreach (const QString& deformerID, _connectionChildMap.values(model.key())) { foreach (const QString& clusterID, _connectionChildMap.values(deformerID)) { if (!clusters.contains(clusterID)) { continue; } QString topID = getTopModelID(_connectionParentMap, models, _connectionChildMap.value(clusterID), url); _connectionChildMap.remove(_connectionParentMap.take(model.key()), model.key()); _connectionParentMap.insert(model.key(), topID); goto outerBreak; } } outerBreak: // make sure the parent is in the child map QString parent = _connectionParentMap.value(model.key()); if (!_connectionChildMap.contains(parent, model.key())) { _connectionChildMap.insert(parent, model.key()); } remainingModels.insert(model.key()); } while (!remainingModels.isEmpty()) { QString first = *remainingModels.constBegin(); foreach (const QString& id, remainingModels) { if (id < first) { first = id; } } QString topID = getTopModelID(_connectionParentMap, models, first, url); appendModelIDs(_connectionParentMap.value(topID), _connectionChildMap, models, remainingModels, modelIDs); } // figure the number of animation frames from the curves int frameCount = 1; foreach (const AnimationCurve& curve, animationCurves) { frameCount = qMax(frameCount, curve.values.size()); } for (int i = 0; i < frameCount; i++) { FBXAnimationFrame frame; frame.rotations.resize(modelIDs.size()); frame.translations.resize(modelIDs.size()); geometry.animationFrames.append(frame); } // convert the models to joints QVariantList freeJoints = mapping.values("freeJoint"); geometry.hasSkeletonJoints = false; foreach (const QString& modelID, modelIDs) { const FBXModel& model = models[modelID]; FBXJoint joint; joint.isFree = freeJoints.contains(model.name); joint.parentIndex = model.parentIndex; // get the indices of all ancestors starting with the first free one (if any) int jointIndex = geometry.joints.size(); joint.freeLineage.append(jointIndex); int lastFreeIndex = joint.isFree ? 0 : -1; for (int index = joint.parentIndex; index != -1; index = geometry.joints.at(index).parentIndex) { if (geometry.joints.at(index).isFree) { lastFreeIndex = joint.freeLineage.size(); } joint.freeLineage.append(index); } joint.freeLineage.remove(lastFreeIndex + 1, joint.freeLineage.size() - lastFreeIndex - 1); joint.translation = model.translation; // these are usually in centimeters joint.preTransform = model.preTransform; joint.preRotation = model.preRotation; joint.rotation = model.rotation; joint.postRotation = model.postRotation; joint.postTransform = model.postTransform; joint.rotationMin = model.rotationMin; joint.rotationMax = model.rotationMax; joint.hasGeometricOffset = model.hasGeometricOffset; joint.geometricTranslation = model.geometricTranslation; joint.geometricRotation = model.geometricRotation; joint.geometricScaling = model.geometricScaling; glm::quat combinedRotation = joint.preRotation * joint.rotation * joint.postRotation; if (joint.parentIndex == -1) { joint.transform = geometry.offset * glm::translate(joint.translation) * joint.preTransform * glm::mat4_cast(combinedRotation) * joint.postTransform; joint.inverseDefaultRotation = glm::inverse(combinedRotation); joint.distanceToParent = 0.0f; } else { const FBXJoint& parentJoint = geometry.joints.at(joint.parentIndex); joint.transform = parentJoint.transform * glm::translate(joint.translation) * joint.preTransform * glm::mat4_cast(combinedRotation) * joint.postTransform; joint.inverseDefaultRotation = glm::inverse(combinedRotation) * parentJoint.inverseDefaultRotation; joint.distanceToParent = glm::distance(extractTranslation(parentJoint.transform), extractTranslation(joint.transform)); } joint.inverseBindRotation = joint.inverseDefaultRotation; joint.name = model.name; foreach (const QString& childID, _connectionChildMap.values(modelID)) { QString type = typeFlags.value(childID); if (!type.isEmpty()) { geometry.hasSkeletonJoints |= (joint.isSkeletonJoint = type.toLower().contains("Skeleton")); break; } } joint.bindTransformFoundInCluster = false; geometry.joints.append(joint); geometry.jointIndices.insert(model.name, geometry.joints.size()); QString rotationID = localRotations.value(modelID); AnimationCurve xRotCurve = animationCurves.value(xComponents.value(rotationID)); AnimationCurve yRotCurve = animationCurves.value(yComponents.value(rotationID)); AnimationCurve zRotCurve = animationCurves.value(zComponents.value(rotationID)); QString translationID = localTranslations.value(modelID); AnimationCurve xPosCurve = animationCurves.value(xComponents.value(translationID)); AnimationCurve yPosCurve = animationCurves.value(yComponents.value(translationID)); AnimationCurve zPosCurve = animationCurves.value(zComponents.value(translationID)); glm::vec3 defaultRotValues = glm::degrees(safeEulerAngles(joint.rotation)); glm::vec3 defaultPosValues = joint.translation; for (int i = 0; i < frameCount; i++) { geometry.animationFrames[i].rotations[jointIndex] = glm::quat(glm::radians(glm::vec3( xRotCurve.values.isEmpty() ? defaultRotValues.x : xRotCurve.values.at(i % xRotCurve.values.size()), yRotCurve.values.isEmpty() ? defaultRotValues.y : yRotCurve.values.at(i % yRotCurve.values.size()), zRotCurve.values.isEmpty() ? defaultRotValues.z : zRotCurve.values.at(i % zRotCurve.values.size())))); geometry.animationFrames[i].translations[jointIndex] = glm::vec3( xPosCurve.values.isEmpty() ? defaultPosValues.x : xPosCurve.values.at(i % xPosCurve.values.size()), yPosCurve.values.isEmpty() ? defaultPosValues.y : yPosCurve.values.at(i % yPosCurve.values.size()), zPosCurve.values.isEmpty() ? defaultPosValues.z : zPosCurve.values.at(i % zPosCurve.values.size())); } } // NOTE: shapeVertices are in joint-frame std::vector shapeVertices; shapeVertices.resize(geometry.joints.size()); // find our special joints geometry.leftEyeJointIndex = modelIDs.indexOf(jointEyeLeftID); geometry.rightEyeJointIndex = modelIDs.indexOf(jointEyeRightID); geometry.neckJointIndex = modelIDs.indexOf(jointNeckID); geometry.rootJointIndex = modelIDs.indexOf(jointRootID); geometry.leanJointIndex = modelIDs.indexOf(jointLeanID); geometry.headJointIndex = modelIDs.indexOf(jointHeadID); geometry.leftHandJointIndex = modelIDs.indexOf(jointLeftHandID); geometry.rightHandJointIndex = modelIDs.indexOf(jointRightHandID); geometry.leftToeJointIndex = modelIDs.indexOf(jointLeftToeID); geometry.rightToeJointIndex = modelIDs.indexOf(jointRightToeID); foreach (const QString& id, humanIKJointIDs) { geometry.humanIKJointIndices.append(modelIDs.indexOf(id)); } // extract the translation component of the neck transform if (geometry.neckJointIndex != -1) { const glm::mat4& transform = geometry.joints.at(geometry.neckJointIndex).transform; geometry.neckPivot = glm::vec3(transform[3][0], transform[3][1], transform[3][2]); } geometry.bindExtents.reset(); geometry.meshExtents.reset(); // Create the Material Library consolidateFBXMaterials(mapping); // We can't allow the scaling of a given image to different sizes, because the hash used for the KTX cache is based on the original image // Allowing scaling of the same image to different sizes would cause different KTX files to target the same cache key #if 0 // HACK: until we get proper LOD management we're going to cap model textures // according to how many unique textures the model uses: // 1 - 8 textures --> 2048 // 8 - 32 textures --> 1024 // 33 - 128 textures --> 512 // etc... QSet uniqueTextures; for (auto& material : _fbxMaterials) { material.getTextureNames(uniqueTextures); } int numTextures = uniqueTextures.size(); const int MAX_NUM_TEXTURES_AT_MAX_RESOLUTION = 8; int maxWidth = sqrt(MAX_NUM_PIXELS_FOR_FBX_TEXTURE); if (numTextures > MAX_NUM_TEXTURES_AT_MAX_RESOLUTION) { int numTextureThreshold = MAX_NUM_TEXTURES_AT_MAX_RESOLUTION; const int MIN_MIP_TEXTURE_WIDTH = 64; do { maxWidth /= 2; numTextureThreshold *= 4; } while (numTextureThreshold < numTextures && maxWidth > MIN_MIP_TEXTURE_WIDTH); qCDebug(modelformat) << "Capped square texture width =" << maxWidth << "for model" << url << "with" << numTextures << "textures"; for (auto& material : _fbxMaterials) { material.setMaxNumPixelsPerTexture(maxWidth * maxWidth); } } #endif geometry.materials = _fbxMaterials; // see if any materials have texture children bool materialsHaveTextures = checkMaterialsHaveTextures(_fbxMaterials, _textureFilenames, _connectionChildMap); for (QMap::iterator it = meshes.begin(); it != meshes.end(); it++) { ExtractedMesh& extracted = it.value(); extracted.mesh.meshExtents.reset(); // accumulate local transforms QString modelID = models.contains(it.key()) ? it.key() : _connectionParentMap.value(it.key()); glm::mat4 modelTransform = getGlobalTransform(_connectionParentMap, models, modelID, geometry.applicationName == "mixamo.com", url); // compute the mesh extents from the transformed vertices foreach (const glm::vec3& vertex, extracted.mesh.vertices) { glm::vec3 transformedVertex = glm::vec3(modelTransform * glm::vec4(vertex, 1.0f)); geometry.meshExtents.minimum = glm::min(geometry.meshExtents.minimum, transformedVertex); geometry.meshExtents.maximum = glm::max(geometry.meshExtents.maximum, transformedVertex); extracted.mesh.meshExtents.minimum = glm::min(extracted.mesh.meshExtents.minimum, transformedVertex); extracted.mesh.meshExtents.maximum = glm::max(extracted.mesh.meshExtents.maximum, transformedVertex); extracted.mesh.modelTransform = modelTransform; } // look for textures, material properties // allocate the Part material library int materialIndex = 0; int textureIndex = 0; bool generateTangents = false; QList children = _connectionChildMap.values(modelID); for (int i = children.size() - 1; i >= 0; i--) { const QString& childID = children.at(i); if (_fbxMaterials.contains(childID)) { // the pure material associated with this part FBXMaterial material = _fbxMaterials.value(childID); for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { if (extracted.partMaterialTextures.at(j).first == materialIndex) { FBXMeshPart& part = extracted.mesh.parts[j]; part.materialID = material.materialID; generateTangents |= material.needTangentSpace(); } } materialIndex++; } else if (_textureFilenames.contains(childID)) { FBXTexture texture = getTexture(childID); for (int j = 0; j < extracted.partMaterialTextures.size(); j++) { int partTexture = extracted.partMaterialTextures.at(j).second; if (partTexture == textureIndex && !(partTexture == 0 && materialsHaveTextures)) { // TODO: DO something here that replaces this legacy code // Maybe create a material just for this part with the correct textures? // extracted.mesh.parts[j].diffuseTexture = texture; } } textureIndex++; } } // if we have a normal map (and texture coordinates), we must compute tangents if (generateTangents && !extracted.mesh.texCoords.isEmpty()) { extracted.mesh.tangents.resize(extracted.mesh.vertices.size()); foreach (const FBXMeshPart& part, extracted.mesh.parts) { for (int i = 0; i < part.quadIndices.size(); i += 4) { setTangents(extracted.mesh, part.quadIndices.at(i), part.quadIndices.at(i + 1)); setTangents(extracted.mesh, part.quadIndices.at(i + 1), part.quadIndices.at(i + 2)); setTangents(extracted.mesh, part.quadIndices.at(i + 2), part.quadIndices.at(i + 3)); setTangents(extracted.mesh, part.quadIndices.at(i + 3), part.quadIndices.at(i)); } // <= size - 3 in order to prevent overflowing triangleIndices when (i % 3) != 0 // This is most likely evidence of a further problem in extractMesh() for (int i = 0; i <= part.triangleIndices.size() - 3; i += 3) { setTangents(extracted.mesh, part.triangleIndices.at(i), part.triangleIndices.at(i + 1)); setTangents(extracted.mesh, part.triangleIndices.at(i + 1), part.triangleIndices.at(i + 2)); setTangents(extracted.mesh, part.triangleIndices.at(i + 2), part.triangleIndices.at(i)); } if ((part.triangleIndices.size() % 3) != 0){ qCDebug(modelformat) << "Error in extractFBXGeometry part.triangleIndices.size() is not divisible by three "; } } } // find the clusters with which the mesh is associated QVector clusterIDs; foreach (const QString& childID, _connectionChildMap.values(it.key())) { foreach (const QString& clusterID, _connectionChildMap.values(childID)) { if (!clusters.contains(clusterID)) { continue; } FBXCluster fbxCluster; const Cluster& cluster = clusters[clusterID]; clusterIDs.append(clusterID); // see http://stackoverflow.com/questions/13566608/loading-skinning-information-from-fbx for a discussion // of skinning information in FBX QString jointID = _connectionChildMap.value(clusterID); fbxCluster.jointIndex = modelIDs.indexOf(jointID); if (fbxCluster.jointIndex == -1) { qCDebug(modelformat) << "Joint not in model list: " << jointID; fbxCluster.jointIndex = 0; } fbxCluster.inverseBindMatrix = glm::inverse(cluster.transformLink) * modelTransform; extracted.mesh.clusters.append(fbxCluster); // override the bind rotation with the transform link FBXJoint& joint = geometry.joints[fbxCluster.jointIndex]; joint.inverseBindRotation = glm::inverse(extractRotation(cluster.transformLink)); joint.bindTransform = cluster.transformLink; joint.bindTransformFoundInCluster = true; // update the bind pose extents glm::vec3 bindTranslation = extractTranslation(geometry.offset * joint.bindTransform); geometry.bindExtents.addPoint(bindTranslation); } } // if we don't have a skinned joint, parent to the model itself if (extracted.mesh.clusters.isEmpty()) { FBXCluster cluster; cluster.jointIndex = modelIDs.indexOf(modelID); if (cluster.jointIndex == -1) { qCDebug(modelformat) << "Model not in model list: " << modelID; cluster.jointIndex = 0; } extracted.mesh.clusters.append(cluster); } // whether we're skinned depends on how many clusters are attached const FBXCluster& firstFBXCluster = extracted.mesh.clusters.at(0); glm::mat4 inverseModelTransform = glm::inverse(modelTransform); if (clusterIDs.size() > 1) { // this is a multi-mesh joint const int WEIGHTS_PER_VERTEX = 4; int numClusterIndices = extracted.mesh.vertices.size() * WEIGHTS_PER_VERTEX; extracted.mesh.clusterIndices.fill(0, numClusterIndices); QVector weightAccumulators; weightAccumulators.fill(0.0f, numClusterIndices); for (int i = 0; i < clusterIDs.size(); i++) { QString clusterID = clusterIDs.at(i); const Cluster& cluster = clusters[clusterID]; const FBXCluster& fbxCluster = extracted.mesh.clusters.at(i); int jointIndex = fbxCluster.jointIndex; FBXJoint& joint = geometry.joints[jointIndex]; glm::mat4 transformJointToMesh = inverseModelTransform * joint.bindTransform; glm::vec3 boneEnd = extractTranslation(transformJointToMesh); glm::vec3 boneBegin = boneEnd; glm::vec3 boneDirection; float boneLength = 0.0f; if (joint.parentIndex != -1) { boneBegin = extractTranslation(inverseModelTransform * geometry.joints[joint.parentIndex].bindTransform); boneDirection = boneEnd - boneBegin; boneLength = glm::length(boneDirection); if (boneLength > EPSILON) { boneDirection /= boneLength; } } float clusterScale = extractUniformScale(fbxCluster.inverseBindMatrix); glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform; ShapeVertices& points = shapeVertices.at(jointIndex); for (int j = 0; j < cluster.indices.size(); j++) { int oldIndex = cluster.indices.at(j); float weight = cluster.weights.at(j); for (QMultiHash::const_iterator it = extracted.newIndices.constFind(oldIndex); it != extracted.newIndices.end() && it.key() == oldIndex; it++) { int newIndex = it.value(); // remember vertices with at least 1/4 weight const float EXPANSION_WEIGHT_THRESHOLD = 0.99f; if (weight > EXPANSION_WEIGHT_THRESHOLD) { // transform to joint-frame and save for later const glm::mat4 vertexTransform = meshToJoint * glm::translate(extracted.mesh.vertices.at(newIndex)); points.push_back(extractTranslation(vertexTransform) * clusterScale); } // look for an unused slot in the weights vector int weightIndex = newIndex * WEIGHTS_PER_VERTEX; int lowestIndex = -1; float lowestWeight = FLT_MAX; int k = 0; for (; k < WEIGHTS_PER_VERTEX; k++) { if (weightAccumulators[weightIndex + k] == 0.0f) { extracted.mesh.clusterIndices[weightIndex + k] = i; weightAccumulators[weightIndex + k] = weight; break; } if (weightAccumulators[weightIndex + k] < lowestWeight) { lowestIndex = k; lowestWeight = weightAccumulators[weightIndex + k]; } } if (k == WEIGHTS_PER_VERTEX && weight > lowestWeight) { // no space for an additional weight; we must replace the lowest weightAccumulators[weightIndex + lowestIndex] = weight; extracted.mesh.clusterIndices[weightIndex + lowestIndex] = i; } } } } // now that we've accumulated the most relevant weights for each vertex // normalize and compress to 8-bits extracted.mesh.clusterWeights.fill(0, numClusterIndices); int numVertices = extracted.mesh.vertices.size(); for (int i = 0; i < numVertices; ++i) { int j = i * WEIGHTS_PER_VERTEX; // normalize weights into uint8_t float totalWeight = weightAccumulators[j]; for (int k = j + 1; k < j + WEIGHTS_PER_VERTEX; ++k) { totalWeight += weightAccumulators[k]; } if (totalWeight > 0.0f) { const float ALMOST_HALF = 0.499f; float weightScalingFactor = (float)(UINT8_MAX) / totalWeight; for (int k = j; k < j + WEIGHTS_PER_VERTEX; ++k) { extracted.mesh.clusterWeights[k] = (uint8_t)(weightScalingFactor * weightAccumulators[k] + ALMOST_HALF); } } } } else { // this is a single-mesh joint int jointIndex = firstFBXCluster.jointIndex; FBXJoint& joint = geometry.joints[jointIndex]; // transform cluster vertices to joint-frame and save for later float clusterScale = extractUniformScale(firstFBXCluster.inverseBindMatrix); glm::mat4 meshToJoint = glm::inverse(joint.bindTransform) * modelTransform; ShapeVertices& points = shapeVertices.at(jointIndex); foreach (const glm::vec3& vertex, extracted.mesh.vertices) { const glm::mat4 vertexTransform = meshToJoint * glm::translate(vertex); points.push_back(extractTranslation(vertexTransform) * clusterScale); } // Apply geometric offset, if present, by transforming the vertices directly if (joint.hasGeometricOffset) { glm::mat4 geometricOffset = createMatFromScaleQuatAndPos(joint.geometricScaling, joint.geometricRotation, joint.geometricTranslation); for (int i = 0; i < extracted.mesh.vertices.size(); i++) { extracted.mesh.vertices[i] = transformPoint(geometricOffset, extracted.mesh.vertices[i]); } } } buildModelMesh(extracted.mesh, url); geometry.meshes.append(extracted.mesh); int meshIndex = geometry.meshes.size() - 1; meshIDsToMeshIndices.insert(it.key(), meshIndex); } const float INV_SQRT_3 = 0.57735026918f; ShapeVertices cardinalDirections = { Vectors::UNIT_X, Vectors::UNIT_Y, Vectors::UNIT_Z, glm::vec3(INV_SQRT_3, INV_SQRT_3, INV_SQRT_3), glm::vec3(INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3), glm::vec3(INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3), glm::vec3(INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3) }; // now that all joints have been scanned compute a k-Dop bounding volume of mesh glm::vec3 defaultCapsuleAxis(0.0f, 1.0f, 0.0f); for (int i = 0; i < geometry.joints.size(); ++i) { FBXJoint& joint = geometry.joints[i]; // NOTE: points are in joint-frame ShapeVertices& points = shapeVertices.at(i); if (points.size() > 0) { // compute average point glm::vec3 avgPoint = glm::vec3(0.0f); for (uint32_t j = 0; j < points.size(); ++j) { avgPoint += points[j]; } avgPoint /= (float)points.size(); // compute a k-Dop bounding volume for (uint32_t j = 0; j < cardinalDirections.size(); ++j) { float maxDot = -FLT_MAX; float minDot = FLT_MIN; for (uint32_t k = 0; k < points.size(); ++k) { float kDot = glm::dot(cardinalDirections[j], points[k] - avgPoint); if (kDot > maxDot) { maxDot = kDot; } if (kDot < minDot) { minDot = kDot; } } joint.shapeInfo.points.push_back(avgPoint + maxDot * cardinalDirections[j]); joint.shapeInfo.points.push_back(avgPoint + minDot * cardinalDirections[j]); } } } geometry.palmDirection = parseVec3(mapping.value("palmDirection", "0, -1, 0").toString()); // attempt to map any meshes to a named model for (QHash::const_iterator m = meshIDsToMeshIndices.constBegin(); m != meshIDsToMeshIndices.constEnd(); m++) { const QString& meshID = m.key(); int meshIndex = m.value(); if (ooChildToParent.contains(meshID)) { const QString& modelID = ooChildToParent.value(meshID); if (modelIDsToNames.contains(modelID)) { const QString& modelName = modelIDsToNames.value(modelID); geometry.meshIndicesToModelNames.insert(meshIndex, modelName); } } } return geometryPtr; } FBXGeometry* readFBX(const QByteArray& model, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { QBuffer buffer(const_cast(&model)); buffer.open(QIODevice::ReadOnly); return readFBX(&buffer, mapping, url, loadLightmaps, lightmapLevel); } FBXGeometry* readFBX(QIODevice* device, const QVariantHash& mapping, const QString& url, bool loadLightmaps, float lightmapLevel) { FBXReader reader; reader._fbxNode = FBXReader::parseFBX(device); reader._loadLightmaps = loadLightmaps; reader._lightmapLevel = lightmapLevel; return reader.extractFBXGeometry(mapping, url); }